@mseep/obsidian-agent-client 0.10.6
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/.claude/hooks/gh-setup.sh +49 -0
- package/.claude/settings.json +15 -0
- package/.claude/skills/release-notes/SKILL.md +331 -0
- package/.editorconfig +10 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
- package/.github/copilot-instructions.md +45 -0
- package/.github/pull_request_template.md +32 -0
- package/.github/workflows/ci.yaml +25 -0
- package/.github/workflows/docs.yml +58 -0
- package/.github/workflows/relay_to_openclaw.yml +59 -0
- package/.github/workflows/release.yaml +45 -0
- package/.prettierignore +10 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +37 -0
- package/.zed/settings.json +42 -0
- package/AGENTS.md +330 -0
- package/ARCHITECTURE.md +390 -0
- package/CONTRIBUTING.md +216 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.ja.md +121 -0
- package/README.md +125 -0
- package/docs/.vitepress/config.mts +124 -0
- package/docs/.vitepress/theme/custom.css +111 -0
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/agent-setup/claude-code.md +84 -0
- package/docs/agent-setup/codex.md +76 -0
- package/docs/agent-setup/custom-agents.md +67 -0
- package/docs/agent-setup/gemini-cli.md +99 -0
- package/docs/agent-setup/index.md +34 -0
- package/docs/announcements/gemini-cli-deprecation.md +73 -0
- package/docs/getting-started/index.md +78 -0
- package/docs/getting-started/quick-start.md +38 -0
- package/docs/help/faq.md +181 -0
- package/docs/help/troubleshooting.md +221 -0
- package/docs/index.md +63 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/demo.mp4 +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/public/images/editing.webp +0 -0
- package/docs/public/images/export.webp +0 -0
- package/docs/public/images/floating-chat-button.webp +0 -0
- package/docs/public/images/floating-chat-instance-menu.webp +0 -0
- package/docs/public/images/floating-chat-view.webp +0 -0
- package/docs/public/images/mode-selection.webp +0 -0
- package/docs/public/images/model-selection.webp +0 -0
- package/docs/public/images/multi-session.webp +0 -0
- package/docs/public/images/remove-image.webp +0 -0
- package/docs/public/images/ribbon-icon.webp +0 -0
- package/docs/public/images/selection-context.gif +0 -0
- package/docs/public/images/sending-images.webp +0 -0
- package/docs/public/images/sending-messages.webp +0 -0
- package/docs/public/images/session-history-button.webp +0 -0
- package/docs/public/images/slash-commands-1.webp +0 -0
- package/docs/public/images/slash-commands-2.webp +0 -0
- package/docs/public/images/switch-agent.webp +0 -0
- package/docs/public/images/switch-default-agent.webp +0 -0
- package/docs/public/images/temporary-disable.gif +0 -0
- package/docs/reference/acp-support.md +110 -0
- package/docs/usage/chat-export.md +80 -0
- package/docs/usage/commands.md +51 -0
- package/docs/usage/context-files.md +57 -0
- package/docs/usage/editing.md +69 -0
- package/docs/usage/floating-chat.md +84 -0
- package/docs/usage/index.md +97 -0
- package/docs/usage/mcp-tools.md +33 -0
- package/docs/usage/mentions.md +70 -0
- package/docs/usage/mode-selection.md +28 -0
- package/docs/usage/model-selection.md +32 -0
- package/docs/usage/multi-session.md +68 -0
- package/docs/usage/sending-images.md +64 -0
- package/docs/usage/session-history.md +91 -0
- package/docs/usage/slash-commands.md +44 -0
- package/esbuild.config.mjs +49 -0
- package/eslint.config.mjs +25 -0
- package/main.js +228 -0
- package/manifest.json +11 -0
- package/package.json +52 -0
- package/src/acp/acp-client.ts +921 -0
- package/src/acp/acp-handler.ts +252 -0
- package/src/acp/permission-handler.ts +282 -0
- package/src/acp/terminal-handler.ts +264 -0
- package/src/acp/type-converter.ts +272 -0
- package/src/hooks/useAgent.ts +250 -0
- package/src/hooks/useAgentMessages.ts +470 -0
- package/src/hooks/useAgentSession.ts +544 -0
- package/src/hooks/useChatActions.ts +400 -0
- package/src/hooks/useHistoryModal.ts +219 -0
- package/src/hooks/useSessionHistory.ts +863 -0
- package/src/hooks/useSettings.ts +19 -0
- package/src/hooks/useSuggestions.ts +342 -0
- package/src/main.ts +9 -0
- package/src/plugin.ts +1126 -0
- package/src/services/chat-exporter.ts +552 -0
- package/src/services/message-sender.ts +755 -0
- package/src/services/message-state.ts +375 -0
- package/src/services/session-helpers.ts +211 -0
- package/src/services/session-state.ts +130 -0
- package/src/services/session-storage.ts +267 -0
- package/src/services/settings-normalizer.ts +255 -0
- package/src/services/settings-service.ts +285 -0
- package/src/services/update-checker.ts +128 -0
- package/src/services/vault-service.ts +558 -0
- package/src/services/view-registry.ts +345 -0
- package/src/types/agent.ts +92 -0
- package/src/types/chat.ts +351 -0
- package/src/types/errors.ts +136 -0
- package/src/types/obsidian-internals.d.ts +14 -0
- package/src/types/session.ts +731 -0
- package/src/ui/ChangeDirectoryModal.ts +137 -0
- package/src/ui/ChatContext.ts +25 -0
- package/src/ui/ChatHeader.tsx +295 -0
- package/src/ui/ChatPanel.tsx +1162 -0
- package/src/ui/ChatView.tsx +348 -0
- package/src/ui/ErrorBanner.tsx +104 -0
- package/src/ui/FloatingButton.tsx +351 -0
- package/src/ui/FloatingChatView.tsx +531 -0
- package/src/ui/InputArea.tsx +1107 -0
- package/src/ui/InputToolbar.tsx +371 -0
- package/src/ui/MessageBubble.tsx +442 -0
- package/src/ui/MessageList.tsx +265 -0
- package/src/ui/PermissionBanner.tsx +61 -0
- package/src/ui/SessionHistoryModal.tsx +821 -0
- package/src/ui/SettingsTab.ts +1337 -0
- package/src/ui/SuggestionPopup.tsx +138 -0
- package/src/ui/TerminalBlock.tsx +107 -0
- package/src/ui/ToolCallBlock.tsx +456 -0
- package/src/ui/shared/AttachmentStrip.tsx +57 -0
- package/src/ui/shared/IconButton.tsx +55 -0
- package/src/ui/shared/MarkdownRenderer.tsx +103 -0
- package/src/ui/view-host.ts +56 -0
- package/src/utils/error-utils.ts +274 -0
- package/src/utils/logger.ts +44 -0
- package/src/utils/mention-parser.ts +129 -0
- package/src/utils/paths.ts +246 -0
- package/src/utils/platform.ts +425 -0
- package/styles.css +2322 -0
- package/tsconfig.json +18 -0
- package/version-bump.mjs +18 -0
- package/versions.json +3 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { Platform } from "obsidian";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shell escaping utilities for different platforms.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Escape a shell argument for Windows cmd.exe.
|
|
10
|
+
* Only wraps in double quotes if the argument contains spaces or special characters.
|
|
11
|
+
*
|
|
12
|
+
* In cmd.exe:
|
|
13
|
+
* - Double quotes are escaped by doubling them: " → ""
|
|
14
|
+
* - Percent signs are escaped by doubling them: % → %% (to prevent environment variable expansion)
|
|
15
|
+
*/
|
|
16
|
+
export function escapeShellArgWindows(arg: string): string {
|
|
17
|
+
// Escape percent signs and double quotes
|
|
18
|
+
const escaped = arg.replace(/%/g, "%%").replace(/"/g, '""');
|
|
19
|
+
|
|
20
|
+
// Only wrap in quotes if contains spaces or special characters that need quoting
|
|
21
|
+
if (/[\s&()<>|^]/.test(arg)) {
|
|
22
|
+
return `"${escaped}"`;
|
|
23
|
+
}
|
|
24
|
+
return escaped;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the login shell for the current platform.
|
|
29
|
+
* Uses $SHELL environment variable when available (covers NixOS, etc.),
|
|
30
|
+
* falls back to platform defaults (/bin/zsh on macOS, /bin/sh on Linux).
|
|
31
|
+
*/
|
|
32
|
+
export function getLoginShell(): string {
|
|
33
|
+
if (process.env.SHELL) {
|
|
34
|
+
return process.env.SHELL;
|
|
35
|
+
}
|
|
36
|
+
return Platform.isMacOS ? "/bin/zsh" : "/bin/sh";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Escape a shell argument for Bash/Zsh/POSIX shells.
|
|
41
|
+
* Wraps the argument in single quotes and escapes internal single quotes
|
|
42
|
+
* using the '\'' idiom (end quote, escaped quote, start quote).
|
|
43
|
+
*
|
|
44
|
+
* Example: hello'world → 'hello'\''world'
|
|
45
|
+
*/
|
|
46
|
+
export function escapeShellArgBash(arg: string): string {
|
|
47
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Cache for the full Windows PATH to avoid repeated registry queries.
|
|
52
|
+
*/
|
|
53
|
+
let cachedFullPath: string | null = null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the full Windows PATH environment variable from the registry.
|
|
57
|
+
*
|
|
58
|
+
* Electron apps launched from shortcuts don't inherit the full user PATH.
|
|
59
|
+
* This function queries both system and user PATH from the registry
|
|
60
|
+
* and combines them to get the complete PATH.
|
|
61
|
+
*
|
|
62
|
+
* @returns The full PATH string, or null if unable to retrieve
|
|
63
|
+
*/
|
|
64
|
+
export function getFullWindowsPath(): string | null {
|
|
65
|
+
if (!Platform.isWin) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (cachedFullPath !== null) {
|
|
70
|
+
return cachedFullPath;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Get system PATH from registry
|
|
75
|
+
const systemPath = execSync(
|
|
76
|
+
'reg query "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v Path',
|
|
77
|
+
{ encoding: "utf8", windowsHide: true },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Get user PATH from registry
|
|
81
|
+
const userPath = execSync('reg query "HKCU\\Environment" /v Path', {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Parse the registry output to extract PATH values
|
|
87
|
+
const systemPathValue = parseRegQueryOutput(systemPath);
|
|
88
|
+
const userPathValue = parseRegQueryOutput(userPath);
|
|
89
|
+
|
|
90
|
+
// Combine system and user PATH (user PATH typically comes first)
|
|
91
|
+
const paths: string[] = [];
|
|
92
|
+
if (userPathValue) {
|
|
93
|
+
paths.push(userPathValue);
|
|
94
|
+
}
|
|
95
|
+
if (systemPathValue) {
|
|
96
|
+
paths.push(systemPathValue);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
cachedFullPath = paths.join(";");
|
|
100
|
+
return cachedFullPath;
|
|
101
|
+
} catch {
|
|
102
|
+
// If registry query fails, return null
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse the output of `reg query` command to extract the PATH value.
|
|
109
|
+
*/
|
|
110
|
+
function parseRegQueryOutput(output: string): string | null {
|
|
111
|
+
// Registry output format:
|
|
112
|
+
// HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
|
|
113
|
+
// Path REG_EXPAND_SZ C:\Windows\system32;C:\Windows;...
|
|
114
|
+
const lines = output.split("\n");
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const trimmed = line.trim();
|
|
117
|
+
// Look for lines containing "Path" and "REG_"
|
|
118
|
+
if (trimmed.toLowerCase().startsWith("path")) {
|
|
119
|
+
// Split by REG_SZ or REG_EXPAND_SZ and take the value part
|
|
120
|
+
const match = trimmed.match(/Path\s+REG_(?:EXPAND_)?SZ\s+(.+)/i);
|
|
121
|
+
if (match) {
|
|
122
|
+
return match[1].trim();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get enhanced environment variables for Windows.
|
|
131
|
+
*
|
|
132
|
+
* This merges the current process.env with the full PATH from registry,
|
|
133
|
+
* ensuring that executables like python, node, etc. can be found.
|
|
134
|
+
*
|
|
135
|
+
* @param baseEnv - The base environment variables to enhance
|
|
136
|
+
* @returns Enhanced environment variables with full PATH
|
|
137
|
+
*/
|
|
138
|
+
export function getEnhancedWindowsEnv(
|
|
139
|
+
baseEnv: NodeJS.ProcessEnv,
|
|
140
|
+
): NodeJS.ProcessEnv {
|
|
141
|
+
if (!Platform.isWin) {
|
|
142
|
+
return baseEnv;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const fullPath = getFullWindowsPath();
|
|
146
|
+
if (!fullPath) {
|
|
147
|
+
return baseEnv;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Merge the full PATH with any existing PATH modifications
|
|
151
|
+
const existingPath = baseEnv.PATH || "";
|
|
152
|
+
const existingPaths = existingPath.split(";").filter((p) => p.length > 0);
|
|
153
|
+
const fullPaths = fullPath.split(";").filter((p) => p.length > 0);
|
|
154
|
+
|
|
155
|
+
// Combine: keep existing modifications first, then add paths from registry
|
|
156
|
+
// that aren't already present
|
|
157
|
+
const combinedPaths = [...existingPaths];
|
|
158
|
+
for (const p of fullPaths) {
|
|
159
|
+
if (!combinedPaths.some((ep) => ep.toLowerCase() === p.toLowerCase())) {
|
|
160
|
+
combinedPaths.push(p);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...baseEnv,
|
|
166
|
+
PATH: combinedPaths.join(";"),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear the cached PATH (useful for testing or when PATH might have changed).
|
|
172
|
+
*/
|
|
173
|
+
export function clearWindowsPathCache(): void {
|
|
174
|
+
cachedFullPath = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Convert Windows path to WSL path format.
|
|
179
|
+
* Example: C:\Users\name\vault → /mnt/c/Users/name/vault
|
|
180
|
+
*
|
|
181
|
+
* Note: This function is only called in WSL mode on Windows.
|
|
182
|
+
*/
|
|
183
|
+
export function convertWindowsPathToWsl(windowsPath: string): string {
|
|
184
|
+
// Normalize backslashes to forward slashes
|
|
185
|
+
const normalized = windowsPath.replace(/\\/g, "/");
|
|
186
|
+
|
|
187
|
+
// Match drive letter pattern: C:/... or C:\...
|
|
188
|
+
const match = normalized.match(/^([A-Za-z]):(\/.*)/);
|
|
189
|
+
|
|
190
|
+
if (match) {
|
|
191
|
+
const driveLetter = match[1].toLowerCase();
|
|
192
|
+
const pathPart = match[2];
|
|
193
|
+
return `/mnt/${driveLetter}${pathPart}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return windowsPath;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert WSL path to Windows path format.
|
|
201
|
+
* Example: /mnt/c/Users/name/vault → C:\Users\name\vault
|
|
202
|
+
*
|
|
203
|
+
* Note: This function is only called in WSL mode on Windows.
|
|
204
|
+
*/
|
|
205
|
+
export function convertWslPathToWindows(wslPath: string): string {
|
|
206
|
+
const match = wslPath.match(/^\/mnt\/([a-zA-Z])\/(.*)/);
|
|
207
|
+
|
|
208
|
+
if (match) {
|
|
209
|
+
const driveLetter = match[1].toUpperCase();
|
|
210
|
+
const pathPart = match[2].replace(/\//g, "\\");
|
|
211
|
+
return `${driveLetter}:\\${pathPart}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return wslPath;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Compare two directory paths accounting for Windows ↔ WSL format differences.
|
|
219
|
+
*/
|
|
220
|
+
export function isSameDirectory(pathA: string, pathB: string): boolean {
|
|
221
|
+
if (pathA === pathB) return true;
|
|
222
|
+
|
|
223
|
+
const normalize = (p: string): string => {
|
|
224
|
+
const win = convertWslPathToWindows(p);
|
|
225
|
+
return win.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const a = normalize(pathA);
|
|
229
|
+
const b = normalize(pathB);
|
|
230
|
+
|
|
231
|
+
return Platform.isWin ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build a WSL shell wrapper that sources ~/.profile, detects the user's
|
|
236
|
+
* $SHELL, and falls back to /bin/sh for non-POSIX shells (fish, elvish,
|
|
237
|
+
* nushell, xonsh).
|
|
238
|
+
*
|
|
239
|
+
* IMPORTANT: wsl.exe pre-expands $VAR references using WSL environment
|
|
240
|
+
* variables before passing them to the Linux shell. Intermediate variables
|
|
241
|
+
* (e.g., s=$SHELL; exec $s) will NOT work because wsl.exe expands $s to
|
|
242
|
+
* empty. Always reference $SHELL or ${SHELL:-/bin/sh} directly.
|
|
243
|
+
*
|
|
244
|
+
* @param innerCommand - The POSIX command to execute inside the login shell
|
|
245
|
+
* @returns The full wrapper command string to pass as argument to `sh -c`
|
|
246
|
+
*/
|
|
247
|
+
export function buildWslShellWrapper(innerCommand: string): string {
|
|
248
|
+
const innerEscaped = innerCommand.replace(/'/g, "'\\''");
|
|
249
|
+
return (
|
|
250
|
+
`. ~/.profile 2>/dev/null; ` +
|
|
251
|
+
`case \${SHELL:-/bin/sh} in ` +
|
|
252
|
+
`*/fish|*/elvish|*/nushell|*/xonsh) exec /bin/sh -l -c '${innerEscaped}';; ` +
|
|
253
|
+
`*) exec \${SHELL:-/bin/sh} -l -c '${innerEscaped}';; ` +
|
|
254
|
+
`esac`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Wrap a command to run inside WSL using wsl.exe.
|
|
260
|
+
* Generates wsl.exe command with proper arguments for executing commands in WSL environment.
|
|
261
|
+
*/
|
|
262
|
+
export function wrapCommandForWsl(
|
|
263
|
+
command: string,
|
|
264
|
+
args: string[],
|
|
265
|
+
cwd: string,
|
|
266
|
+
distribution?: string,
|
|
267
|
+
additionalPath?: string,
|
|
268
|
+
): { command: string; args: string[] } {
|
|
269
|
+
// Validate working directory path
|
|
270
|
+
// Check for UNC paths (\\server\share) which are not supported by WSL
|
|
271
|
+
if (/^\\\\/.test(cwd)) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`UNC paths are not supported in WSL mode: ${cwd}. Please use a local drive path.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const wslCwd = convertWindowsPathToWsl(cwd);
|
|
278
|
+
|
|
279
|
+
// Verify path conversion succeeded (if it was a Windows path with drive letter)
|
|
280
|
+
// If conversion failed, wslCwd will be the same as cwd but still match Windows path pattern
|
|
281
|
+
if (wslCwd === cwd && /^[A-Za-z]:[\\/]/.test(cwd)) {
|
|
282
|
+
throw new Error(`Failed to convert Windows path to WSL format: ${cwd}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Build wsl.exe arguments
|
|
286
|
+
const wslArgs: string[] = [];
|
|
287
|
+
|
|
288
|
+
// Specify WSL distribution if provided
|
|
289
|
+
if (distribution) {
|
|
290
|
+
// Validate distribution name (alphanumeric, dot, dash, underscore only)
|
|
291
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(distribution)) {
|
|
292
|
+
throw new Error(`Invalid WSL distribution name: ${distribution}`);
|
|
293
|
+
}
|
|
294
|
+
wslArgs.push("-d", distribution);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Build command to execute inside WSL
|
|
298
|
+
// Use login shell (-l) to inherit PATH from user's shell profile
|
|
299
|
+
const escapedArgs = args.map(escapeShellArgBash).join(" ");
|
|
300
|
+
const argsString = escapedArgs.length > 0 ? ` ${escapedArgs}` : "";
|
|
301
|
+
|
|
302
|
+
// Add additional PATH if provided (e.g., for Node.js)
|
|
303
|
+
let pathPrefix = "";
|
|
304
|
+
if (additionalPath) {
|
|
305
|
+
const wslPath = convertWindowsPathToWsl(additionalPath);
|
|
306
|
+
// Quote PATH value to handle paths with spaces
|
|
307
|
+
pathPrefix = `export PATH="${escapePathForShell(wslPath)}:$PATH"; `;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const innerCommand = `${pathPrefix}cd ${escapeShellArgBash(wslCwd)} && ${command}${argsString}`;
|
|
311
|
+
wslArgs.push("sh", "-c", buildWslShellWrapper(innerCommand));
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
command: "C:\\Windows\\System32\\wsl.exe",
|
|
315
|
+
args: wslArgs,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Escape a path value for use in shell PATH variable (double-quoted context).
|
|
321
|
+
* Escapes double quotes and backslashes for use within double quotes.
|
|
322
|
+
*/
|
|
323
|
+
function escapePathForShell(path: string): string {
|
|
324
|
+
return path.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Result of platform-specific command preparation.
|
|
329
|
+
*/
|
|
330
|
+
export interface PreparedCommand {
|
|
331
|
+
/** The command to pass to spawn() */
|
|
332
|
+
command: string;
|
|
333
|
+
/** The arguments to pass to spawn() */
|
|
334
|
+
args: string[];
|
|
335
|
+
/** Whether spawn() should use shell: true (Windows non-WSL only) */
|
|
336
|
+
needsShell: boolean;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Prepare a command for execution by wrapping it in the appropriate
|
|
341
|
+
* platform-specific shell.
|
|
342
|
+
*
|
|
343
|
+
* - **WSL**: Wraps via wrapCommandForWsl (wsl.exe → sh -c → login shell)
|
|
344
|
+
* - **macOS/Linux**: Wraps in login shell (-l -c) with optional PATH injection
|
|
345
|
+
* - **Windows non-WSL**: Escapes for cmd.exe (shell: true)
|
|
346
|
+
*
|
|
347
|
+
* @param command - The command to execute
|
|
348
|
+
* @param args - Command arguments
|
|
349
|
+
* @param cwd - Working directory
|
|
350
|
+
* @param options - Platform and configuration options
|
|
351
|
+
* @returns Prepared command ready for spawn()
|
|
352
|
+
*/
|
|
353
|
+
export function prepareShellCommand(
|
|
354
|
+
command: string,
|
|
355
|
+
args: string[],
|
|
356
|
+
cwd: string,
|
|
357
|
+
options: {
|
|
358
|
+
/** Whether WSL mode is enabled (Windows only) */
|
|
359
|
+
wslMode: boolean;
|
|
360
|
+
/** WSL distribution name */
|
|
361
|
+
wslDistribution?: string;
|
|
362
|
+
/** Node.js directory to inject into PATH (absolute path only) */
|
|
363
|
+
nodeDir?: string;
|
|
364
|
+
/**
|
|
365
|
+
* When true, always escape command and args with single quotes.
|
|
366
|
+
* When false, pass command as-is if args is empty (allows shell
|
|
367
|
+
* to parse pipes, &&, etc. in tool_call commands).
|
|
368
|
+
* Default: true
|
|
369
|
+
*/
|
|
370
|
+
alwaysEscape?: boolean;
|
|
371
|
+
},
|
|
372
|
+
): PreparedCommand {
|
|
373
|
+
const alwaysEscape = options.alwaysEscape ?? true;
|
|
374
|
+
|
|
375
|
+
// WSL mode (Windows only)
|
|
376
|
+
if (Platform.isWin && options.wslMode) {
|
|
377
|
+
const wrapped = wrapCommandForWsl(
|
|
378
|
+
command,
|
|
379
|
+
args,
|
|
380
|
+
cwd,
|
|
381
|
+
options.wslDistribution,
|
|
382
|
+
options.nodeDir,
|
|
383
|
+
);
|
|
384
|
+
return {
|
|
385
|
+
command: wrapped.command,
|
|
386
|
+
args: wrapped.args,
|
|
387
|
+
needsShell: false,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// macOS / Linux — login shell
|
|
392
|
+
if (Platform.isMacOS || Platform.isLinux) {
|
|
393
|
+
const shell = getLoginShell();
|
|
394
|
+
let commandString: string;
|
|
395
|
+
if (args.length > 0 || alwaysEscape) {
|
|
396
|
+
commandString = [command, ...args]
|
|
397
|
+
.map(escapeShellArgBash)
|
|
398
|
+
.join(" ");
|
|
399
|
+
} else {
|
|
400
|
+
commandString = command;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Prepend PATH export if nodeDir is provided
|
|
404
|
+
if (options.nodeDir) {
|
|
405
|
+
const escapedNodeDir = options.nodeDir.replace(/'/g, "'\\''");
|
|
406
|
+
commandString = `export PATH='${escapedNodeDir}':"$PATH"; ${commandString}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
command: shell,
|
|
411
|
+
args: ["-l", "-c", commandString],
|
|
412
|
+
needsShell: false,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Windows (non-WSL) — cmd.exe
|
|
417
|
+
if (args.length > 0 || alwaysEscape) {
|
|
418
|
+
return {
|
|
419
|
+
command: escapeShellArgWindows(command),
|
|
420
|
+
args: args.map(escapeShellArgWindows),
|
|
421
|
+
needsShell: true,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return { command, args, needsShell: true };
|
|
425
|
+
}
|