diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts index 37535dce..14effe8e 100644 --- a/apps/server/src/providers/cursor-provider.ts +++ b/apps/server/src/providers/cursor-provider.ts @@ -114,24 +114,30 @@ export class CursorProvider extends CliProvider { }; } - buildCliArgs(options: ExecuteOptions): string[] { - // Extract model (strip 'cursor-' prefix if present) - const model = stripProviderPrefix(options.model || 'auto'); - - // Build prompt content - let promptText: string; + /** + * Extract prompt text from ExecuteOptions + * Used to pass prompt via stdin instead of CLI args to avoid shell escaping issues + */ + private extractPromptText(options: ExecuteOptions): string { if (typeof options.prompt === 'string') { - promptText = options.prompt; + return options.prompt; } else if (Array.isArray(options.prompt)) { - promptText = options.prompt + return options.prompt .filter((p) => p.type === 'text' && p.text) .map((p) => p.text) .join('\n'); } else { throw new Error('Invalid prompt format'); } + } + + buildCliArgs(options: ExecuteOptions): string[] { + // Extract model (strip 'cursor-' prefix if present) + const model = stripProviderPrefix(options.model || 'auto'); // Build CLI arguments for cursor-agent + // NOTE: Prompt is NOT included here - it's passed via stdin to avoid + // shell escaping issues when content contains $(), backticks, etc. const cliArgs: string[] = [ '-p', // Print mode (non-interactive) '--force', // Allow file modifications @@ -145,8 +151,8 @@ export class CursorProvider extends CliProvider { cliArgs.push('--model', model); } - // Add the prompt - cliArgs.push(promptText); + // Use '-' to indicate reading prompt from stdin + cliArgs.push('-'); return cliArgs; } @@ -439,9 +445,16 @@ export class CursorProvider extends CliProvider { ); } + // Extract prompt text to pass via stdin (avoids shell escaping issues) + const promptText = this.extractPromptText(options); + const cliArgs = this.buildCliArgs(options); const subprocessOptions = this.buildSubprocessOptions(options, cliArgs); + // Pass prompt via stdin to avoid shell interpretation of special characters + // like $(), backticks, etc. that may appear in file content + subprocessOptions.stdinData = promptText; + let sessionId: string | undefined; // Dedup state for Cursor-specific text block handling diff --git a/libs/platform/src/subprocess.ts b/libs/platform/src/subprocess.ts index 4c5afcb0..011d8a62 100644 --- a/libs/platform/src/subprocess.ts +++ b/libs/platform/src/subprocess.ts @@ -12,6 +12,12 @@ export interface SubprocessOptions { env?: Record; abortController?: AbortController; timeout?: number; // Milliseconds of no output before timeout + /** + * Data to write to stdin after process spawns. + * Use this for passing prompts/content that may contain shell metacharacters. + * Avoids shell interpretation issues when passing data as CLI arguments. + */ + stdinData?: string; } export interface SubprocessResult { @@ -24,22 +30,33 @@ export interface SubprocessResult { * Spawns a subprocess and streams JSONL output line-by-line */ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGenerator { - const { command, args, cwd, env, abortController, timeout = 30000 } = options; + const { command, args, cwd, env, abortController, timeout = 30000, stdinData } = options; const processEnv = { ...process.env, ...env, }; - console.log(`[SubprocessManager] Spawning: ${command} ${args.slice(0, -1).join(' ')}`); + // Log command without stdin data (which may be large/sensitive) + console.log(`[SubprocessManager] Spawning: ${command} ${args.join(' ')}`); console.log(`[SubprocessManager] Working directory: ${cwd}`); + if (stdinData) { + console.log(`[SubprocessManager] Passing ${stdinData.length} bytes via stdin`); + } const childProcess: ChildProcess = spawn(command, args, { cwd, env: processEnv, - stdio: ['ignore', 'pipe', 'pipe'], + // Use 'pipe' for stdin when we need to write data, otherwise 'ignore' + stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'], }); + // Write stdin data if provided + if (stdinData && childProcess.stdin) { + childProcess.stdin.write(stdinData); + childProcess.stdin.end(); + } + let stderrOutput = ''; let lastOutputTime = Date.now(); let timeoutHandle: NodeJS.Timeout | null = null;