-
Notifications
You must be signed in to change notification settings - Fork 590
fix cursor api prompt errors #838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
e890529
41a4465
40a4342
8dda441
6cc067a
844aee4
71385c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Ensure shell scripts use LF for Docker/Linux compatibility | ||
| docker-entrypoint.sh text eol=lf |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,15 @@ import { execSync } from 'child_process'; | |
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import * as os from 'os'; | ||
| import { findCliInWsl, isWslAvailable } from '@automaker/platform'; | ||
| import { | ||
| findCliInWsl, | ||
| isWslAvailable, | ||
| windowsToWslPath, | ||
| spawnJSONLProcess, | ||
| execInWsl, | ||
| getAutomakerDir, | ||
| type SubprocessOptions, | ||
| } from '@automaker/platform'; | ||
| import { | ||
| CliProvider, | ||
| type CliSpawnConfig, | ||
|
|
@@ -29,7 +37,7 @@ import type { | |
| ModelDefinition, | ||
| ContentBlock, | ||
| } from './types.js'; | ||
| import { validateBareModelId } from '@automaker/types'; | ||
| import { validateBareModelId, calculateReasoningTimeout } from '@automaker/types'; | ||
| import { validateApiKey } from '../lib/auth-utils.js'; | ||
| import { getEffectivePermissions, detectProfile } from '../services/cursor-config-service.js'; | ||
| import { | ||
|
|
@@ -42,7 +50,7 @@ import { | |
| CURSOR_MODEL_MAP, | ||
| } from '@automaker/types'; | ||
| import { createLogger, isAbortError } from '@automaker/utils'; | ||
| import { spawnJSONLProcess, execInWsl } from '@automaker/platform'; | ||
| import { randomBytes } from 'crypto'; | ||
|
|
||
| // Create logger for this module | ||
| const logger = createLogger('CursorProvider'); | ||
|
|
@@ -209,9 +217,9 @@ function formatCursorToolResult(toolCall: CursorToolCallEvent['tool_call']): str | |
| for (const [key, handler] of Object.entries(CURSOR_TOOL_HANDLERS)) { | ||
| const toolData = toolCall[key as keyof typeof toolCall] as | ||
| | { | ||
| args?: unknown; | ||
| result?: { success?: unknown; rejected?: { reason: string } }; | ||
| } | ||
| args?: unknown; | ||
| result?: { success?: unknown; rejected?: { reason: string } }; | ||
| } | ||
| | undefined; | ||
|
|
||
| if (toolData?.result) { | ||
|
|
@@ -399,9 +407,14 @@ export class CursorProvider extends CliProvider { | |
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Max prompt length to pass as CLI arg. Longer prompts use a temp file to avoid | ||
| * platform command-line limits (~32KB Windows, ~2MB Linux). | ||
| */ | ||
| private static readonly PROMPT_ARG_MAX_LENGTH = 24 * 1024; | ||
|
|
||
| /** | ||
| * 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') { | ||
|
|
@@ -416,13 +429,17 @@ export class CursorProvider extends CliProvider { | |
| } | ||
| } | ||
|
|
||
| buildCliArgs(options: ExecuteOptions): string[] { | ||
| buildCliArgs( | ||
| options: ExecuteOptions, | ||
| extras?: { omitPrompt?: boolean } | ||
| ): string[] { | ||
| // Model is already bare (no prefix) - validated by executeQuery | ||
| const model = 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. | ||
| // Build CLI arguments for cursor-agent. | ||
| // NOTE: cursor-agent does NOT support reading from stdin (unlike codex). Passing "-" | ||
| // as the prompt causes it to literally receive "-" as the message ("your message was | ||
| // just a dash"). We pass the prompt as the final positional argument instead. | ||
| const cliArgs: string[] = []; | ||
|
|
||
| // If using Cursor IDE (cliPath is 'cursor' not 'cursor-agent'), add 'agent' subcommand | ||
|
|
@@ -437,10 +454,11 @@ export class CursorProvider extends CliProvider { | |
| '--stream-partial-output' // Real-time streaming | ||
| ); | ||
|
|
||
| // In read-only mode, use --mode ask for Q&A style (no tools) | ||
| // Otherwise, add --force to allow file edits | ||
| // In read-only mode, use --mode ask for Q&A style (no tools). | ||
| // Add --trust so cursor-agent runs non-interactively without prompting (e.g., in Docker). | ||
| // Otherwise, add --force to allow file edits (also satisfies workspace trust). | ||
| if (options.readOnly) { | ||
| cliArgs.push('--mode', 'ask'); | ||
| cliArgs.push('--mode', 'ask', '--trust'); | ||
| } else { | ||
| cliArgs.push('--force'); | ||
| } | ||
|
|
@@ -455,12 +473,96 @@ export class CursorProvider extends CliProvider { | |
| cliArgs.push('--resume', options.sdkSessionId); | ||
| } | ||
|
|
||
| // Use '-' to indicate reading prompt from stdin | ||
| cliArgs.push('-'); | ||
| // Pass prompt as final positional argument (cursor-agent has no stdin support for prompts). | ||
| // Omit when using temp-file fallback for long prompts. | ||
| if (!extras?.omitPrompt) { | ||
| cliArgs.push(this.extractPromptText(options)); | ||
| } | ||
|
|
||
| return cliArgs; | ||
| } | ||
|
|
||
| /** | ||
| * Escape a string for use inside a bash single-quoted literal. | ||
| * Single quotes are escaped as '\'' (end quote, escaped quote, start quote). | ||
| */ | ||
| private escapeForBashSingleQuoted(s: string): string { | ||
| return "'" + s.replace(/'/g, "'\\''") + "'"; | ||
| } | ||
|
|
||
| /** | ||
| * Build subprocess options when using a temp file for the prompt. | ||
| * Runs cursor-agent via bash -c '... "$(cat /path)"' to avoid command-line length limits. | ||
| */ | ||
| private buildSubprocessOptionsWithPromptFile( | ||
| options: ExecuteOptions, | ||
| cliArgs: string[], | ||
| promptFilePath: string | ||
| ): SubprocessOptions { | ||
| this.ensureCliDetected(); | ||
| if (!this.cliPath) { | ||
| throw new Error(`${this.getCliName()} CLI not found. ${this.getInstallInstructions()}`); | ||
| } | ||
|
|
||
| const cwd = options.cwd || process.cwd(); | ||
| const timeout = calculateReasoningTimeout(options.reasoningEffort, 120000); | ||
|
|
||
| const filteredEnv: Record<string, string> = {}; | ||
| for (const [key, value] of Object.entries(process.env)) { | ||
| if (value !== undefined) { | ||
| filteredEnv[key] = value; | ||
| } | ||
| } | ||
|
|
||
| const escapedArgs = cliArgs.map((a) => this.escapeForBashSingleQuoted(a)).join(' '); | ||
| const escapedPath = this.escapeForBashSingleQuoted(promptFilePath); | ||
|
|
||
| if (this.useWsl && this.wslCliPath) { | ||
| const wslCwd = windowsToWslPath(cwd); | ||
| const wslPromptPath = windowsToWslPath(promptFilePath); | ||
| const wslEscapedPath = this.escapeForBashSingleQuoted(wslPromptPath); | ||
| const fullCommand = `${this.escapeForBashSingleQuoted(this.wslCliPath)} ${escapedArgs} "$(cat ${wslEscapedPath})"`; | ||
|
|
||
| const wslArgs = this.wslDistribution | ||
| ? ['-d', this.wslDistribution, '--cd', wslCwd, 'bash', '-c', fullCommand] | ||
| : ['--cd', wslCwd, 'bash', '-c', fullCommand]; | ||
|
|
||
| return { | ||
| command: 'wsl.exe', | ||
| args: wslArgs, | ||
| cwd, | ||
| env: filteredEnv, | ||
| abortController: options.abortController, | ||
| timeout, | ||
| }; | ||
| } | ||
|
|
||
| // Native (Linux/macOS) or npx/direct on Windows | ||
| const cliPath = this.cliPath; | ||
| const fullCommand = `${this.escapeForBashSingleQuoted(cliPath)} ${escapedArgs} "$(cat ${escapedPath})"`; | ||
|
|
||
| if (this.detectedStrategy === 'npx') { | ||
| const allArgs = [...this.npxArgs, 'bash', '-c', fullCommand]; | ||
| return { | ||
| command: 'npx', | ||
| args: allArgs, | ||
| cwd, | ||
| env: filteredEnv, | ||
| abortController: options.abortController, | ||
| timeout, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| command: 'bash', | ||
| args: ['-c', fullCommand], | ||
| cwd, | ||
| env: filteredEnv, | ||
| abortController: options.abortController, | ||
| timeout, | ||
| }; | ||
|
Comment on lines
+540
to
+566
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
printf '\nWindows detection branches:\n'
rg -n -C2 "useWsl: false|strategy: 'cmd'|strategy: 'direct'" apps/server/src/providers/cursor-provider.ts
printf '\nTemp-file subprocess branches:\n'
rg -n -C2 "buildSubprocessOptionsWithPromptFile|command: 'bash'|command: 'wsl.exe'" apps/server/src/providers/cursor-provider.tsRepository: AutoMaker-Org/automaker Length of output: 1606 🏁 Script executed: sed -n '497,563p' apps/server/src/providers/cursor-provider.tsRepository: AutoMaker-Org/automaker Length of output: 2242 🏁 Script executed: sed -n '400,500p' apps/server/src/providers/cursor-provider.ts | head -80Repository: AutoMaker-Org/automaker Length of output: 2726 🏁 Script executed: rg -n "buildSubprocessOptions[^W]|strategy.*cmd|strategy.*direct" apps/server/src/providers/cursor-provider.ts | head -20Repository: AutoMaker-Org/automaker Length of output: 468 🏁 Script executed: rg -n "private buildSubprocessOptions\(" apps/server/src/providers/cursor-provider.ts -A 100 | head -150Repository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: rg -n "buildSubprocessOptions" apps/server/src/providers/cursor-provider.tsRepository: AutoMaker-Org/automaker Length of output: 348 🏁 Script executed: rg -n "buildSubprocessOptions[^W]" apps/server/src/providers/cursor-provider.ts -B 2 -A 50Repository: AutoMaker-Org/automaker Length of output: 3550 🏁 Script executed: grep -n "buildSubprocessOptions" apps/server/src/providers/cursor-provider.ts | grep -v "buildSubprocessOptionsWithPromptFile"Repository: AutoMaker-Org/automaker Length of output: 223 🏁 Script executed: rg -n "^\s*(private|public|protected).*buildSubprocessOptions" apps/server/src/providers/cursor-provider.tsRepository: AutoMaker-Org/automaker Length of output: 118 🏁 Script executed: rg -n "buildSubprocessOptions\s*\(" apps/server/src/providers/cursor-provider.ts -B 5 | grep -E "(private|public|protected|buildSubprocessOptions)" | head -20Repository: AutoMaker-Org/automaker Length of output: 144 🏁 Script executed: grep -n "class.*CursorProvider\|extends" apps/server/src/providers/cursor-provider.ts | head -5Repository: AutoMaker-Org/automaker Length of output: 169 🏁 Script executed: rg -l "class CliProvider" apps/server/src/Repository: AutoMaker-Org/automaker Length of output: 108 🏁 Script executed: rg -n "buildSubprocessOptions" apps/server/src/providers/ -lRepository: AutoMaker-Org/automaker Length of output: 245 🏁 Script executed: rg -n "buildSubprocessOptions" apps/server/src/providers/cli-provider.ts -A 50 | head -100Repository: AutoMaker-Org/automaker Length of output: 3838 🏁 Script executed: rg -n "buildSubprocessOptions" apps/server/src/providers/cli-provider.ts -A 80 | tail -50Repository: AutoMaker-Org/automaker Length of output: 1883 🏁 Script executed: sed -n '446,510p' apps/server/src/providers/cli-provider.tsRepository: AutoMaker-Org/automaker Length of output: 2209 🏁 Script executed: sed -n '510,525p' apps/server/src/providers/cli-provider.tsRepository: AutoMaker-Org/automaker Length of output: 441 Add handler for native Windows (cmd/direct) strategy in The method 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * Convert Cursor event to AutoMaker ProviderMessage format | ||
| * Made public as required by CliProvider abstract method | ||
|
|
@@ -503,7 +605,7 @@ export class CursorProvider extends CliProvider { | |
| const toolCallKeys = Object.keys(toolCall); | ||
| logger.warn( | ||
| `[UNHANDLED TOOL_CALL] Unknown tool call structure. Keys: ${toolCallKeys.join(', ')}. ` + | ||
| `Full tool_call: ${JSON.stringify(toolCall).substring(0, 500)}` | ||
| `Full tool_call: ${JSON.stringify(toolCall).substring(0, 500)}` | ||
| ); | ||
| return null; | ||
| } | ||
|
|
@@ -861,23 +963,45 @@ export class CursorProvider extends CliProvider { | |
| const serverCount = Object.keys(options.mcpServers).length; | ||
| logger.warn( | ||
| `MCP servers configured (${serverCount}) but not yet supported by Cursor CLI in AutoMaker. ` + | ||
| `MCP support for Cursor will be added in a future release. ` + | ||
| `The configured MCP servers will be ignored for this execution.` | ||
| `MCP support for Cursor will be added in a future release. ` + | ||
| `The configured MCP servers will be ignored for this execution.` | ||
| ); | ||
| } | ||
|
|
||
| // Embed system prompt into user prompt (Cursor CLI doesn't support separate system messages) | ||
| const effectiveOptions = this.embedSystemPromptIntoPrompt(options); | ||
|
|
||
| // Extract prompt text to pass via stdin (avoids shell escaping issues) | ||
| // Extract prompt text (cursor-agent expects it as positional arg, not stdin) | ||
| const promptText = this.extractPromptText(effectiveOptions); | ||
|
|
||
| const cliArgs = this.buildCliArgs(effectiveOptions); | ||
| const subprocessOptions = this.buildSubprocessOptions(options, cliArgs); | ||
| const useTempFile = promptText.length > CursorProvider.PROMPT_ARG_MAX_LENGTH; | ||
| let promptFilePath: string | null = null; | ||
|
|
||
| let cliArgs: string[]; | ||
| let subprocessOptions: ReturnType<typeof this.buildSubprocessOptions>; | ||
|
|
||
| if (useTempFile) { | ||
| // Temp file fallback for long prompts (avoids platform command-line limits) | ||
| const cwd = options.cwd || process.cwd(); | ||
| const automakerDir = getAutomakerDir(cwd); | ||
| const promptId = randomBytes(8).toString('hex'); | ||
| promptFilePath = path.join(automakerDir, `.cursor-prompt-${promptId}`); | ||
| fs.mkdirSync(automakerDir, { recursive: true }); | ||
| fs.writeFileSync(promptFilePath, promptText, 'utf8'); | ||
|
Comment on lines
+992
to
+993
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify how temp prompt files are written
rg -n -A2 -B2 "writeFileSync\\(promptFilePath" apps/server/src/providers/cursor-provider.tsRepository: AutoMaker-Org/automaker Length of output: 394 🏁 Script executed: #!/bin/bash
# Find all writeFileSync calls in cursor-provider.ts to see if there are other cases
rg -n "writeFileSync" apps/server/src/providers/cursor-provider.ts
# Also check for other prompt/sensitive file writes in the server directory
rg -n "writeFileSync.*prompt" apps/server/src/Repository: AutoMaker-Org/automaker Length of output: 239 🏁 Script executed: #!/bin/bash
# Check for all writeFileSync calls in cursor-provider.ts to see if other files need protection
rg -n "writeFileSync" apps/server/src/providers/cursor-provider.ts -A 1 -B 1
# Check if there are other sensitive file writes (cache, config, state, etc.) in the file
rg -n "writeFileSync" apps/server/src/providers/cursor-provider.ts | wc -lRepository: AutoMaker-Org/automaker Length of output: 215 🏁 Script executed: #!/bin/bash
# Get more context around the writeFileSync call to understand the sensitivity
sed -n '970,1010p' apps/server/src/providers/cursor-provider.ts
# Also check for other writeFileSync calls in the broader server directory
rg -n "writeFileSync" apps/server/src/ | head -20Repository: AutoMaker-Org/automaker Length of output: 2586 🏁 Script executed: #!/bin/bash
# Check the secure-fs.ts implementation to understand the pattern
cat -n apps/server/src/lib/secure-fs.ts
# Check how auth.ts uses secureFs
grep -n -A 3 -B 3 "secureFs.writeFileSync" apps/server/src/lib/auth.tsRepository: AutoMaker-Org/automaker Length of output: 1360 🏁 Script executed: #!/bin/bash
# Check what fs module is imported in cursor-provider.ts
head -50 apps/server/src/providers/cursor-provider.ts | grep -n "import.*fs"
# Check if cursor-provider.ts should be using secureFs instead
grep -n "^import" apps/server/src/providers/cursor-provider.ts | head -20Repository: AutoMaker-Org/automaker Length of output: 633 🏁 Script executed: #!/bin/bash
# Check the context around the other writeFileSync calls to see if they also write sensitive data
# Start with gemini-usage-service.ts
sed -n '745,755p' apps/server/src/services/gemini-usage-service.ts
# Check cursor-config-manager.ts
sed -n '55,65p' apps/server/src/providers/cursor-config-manager.ts
# Check the deauth files
sed -n '20,30p' apps/server/src/routes/setup/routes/deauth-cursor.tsRepository: AutoMaker-Org/automaker Length of output: 1153 Use secureFs for writing sensitive prompt files with restrictive permissions. These temp prompt files contain sensitive LLM content and should use the established Proposed fix- fs.writeFileSync(promptFilePath, promptText, 'utf8');
+ secureFs.writeFileSync(promptFilePath, promptText, { encoding: 'utf8', mode: 0o600 });Also update the import to use secureFs for this operation (or import it alongside fs). 🤖 Prompt for AI Agents |
||
| logger.debug( | ||
| `Prompt length ${promptText.length} exceeds limit; using temp file: ${promptFilePath}` | ||
| ); | ||
|
|
||
| // Pass prompt via stdin to avoid shell interpretation of special characters | ||
| // like $(), backticks, etc. that may appear in file content | ||
| subprocessOptions.stdinData = promptText; | ||
| cliArgs = this.buildCliArgs(effectiveOptions, { omitPrompt: true }); | ||
| subprocessOptions = this.buildSubprocessOptionsWithPromptFile( | ||
| options, | ||
| cliArgs, | ||
| promptFilePath | ||
| ); | ||
|
Comment on lines
+986
to
+1003
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expand the
Also applies to: 1026-1128 🤖 Prompt for AI Agents |
||
| } else { | ||
| cliArgs = this.buildCliArgs(effectiveOptions); | ||
| subprocessOptions = this.buildSubprocessOptions(options, cliArgs); | ||
| } | ||
|
|
||
| let sessionId: string | undefined; | ||
|
|
||
|
|
@@ -926,8 +1050,8 @@ export class CursorProvider extends CliProvider { | |
| .join(',') || 'unknown'; | ||
| logger.info( | ||
| `[RAW TOOL_CALL] call_id=${toolEvent.call_id} types=[${toolTypes}]` + | ||
| (tc.shellToolCall ? ` cmd="${tc.shellToolCall.args?.command}"` : '') + | ||
| (tc.writeToolCall ? ` path="${tc.writeToolCall.args?.path}"` : '') | ||
| (tc.shellToolCall ? ` cmd="${tc.shellToolCall.args?.command}"` : '') + | ||
| (tc.writeToolCall ? ` path="${tc.writeToolCall.args?.path}"` : '') | ||
| ); | ||
| } | ||
| } | ||
|
|
@@ -993,6 +1117,15 @@ export class CursorProvider extends CliProvider { | |
| ); | ||
| } | ||
| throw error; | ||
| } finally { | ||
| if (promptFilePath) { | ||
| try { | ||
| fs.unlinkSync(promptFilePath); | ||
| logger.debug(`Removed temp prompt file: ${promptFilePath}`); | ||
| } catch { | ||
| // Ignore cleanup errors | ||
| } | ||
KadenHighgate marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.