feat(integrations): support Cursor Agent CLI as non-interactive LLM backend#1149
Conversation
Greptile SummaryThis PR adds Confidence Score: 5/5Safe to merge; all remaining findings are P2 style/UX issues that do not affect correctness. The integration follows the established adapter pattern, env forwarding is minimal and correct, and test coverage is thorough. The two P2 findings (misleading error message when stdout is empty on exit-0, and lack of Cursor-specific validation in the agent binary probe) are quality improvements but do not block functionality. app/integrations/llm_cli/cursor.py — parse() error message and agent binary identity validation. Important Files Changed
Sequence DiagramsequenceDiagram
participant CLI as opensre CLI
participant Runner as CLIBackedLLMClient
participant Adapter as CursorAdapter
participant Proc as agent subprocess
CLI->>Runner: invoke(prompt)
Runner->>Adapter: detect()
Adapter->>Proc: agent --version
Proc-->>Adapter: version stdout
Adapter->>Proc: agent status (if no CURSOR_API_KEY)
Proc-->>Adapter: Logged in as ...
Adapter-->>Runner: CLIProbe(installed=True, logged_in=True)
Runner->>Adapter: build(prompt, model, workspace)
Adapter-->>Runner: CLIInvocation(argv=[agent,--print,--trust,...], stdin=prompt)
Runner->>Proc: subprocess.run(argv, env=filtered_env)
Proc-->>Runner: stdout, returncode=0
Runner->>Adapter: parse(stdout, stderr, returncode)
Adapter-->>Runner: stripped answer
Runner-->>CLI: LLMResponse(content=answer)
Reviews (1): Last reviewed commit: "feat: add Cursor CLI adapter (non-intera..." | Re-trigger Greptile |
| def parse(self, *, stdout: str, stderr: str, returncode: int) -> str: | ||
| result = (stdout or "").strip() | ||
| if not result: | ||
| raise RuntimeError( | ||
| self.explain_failure(stdout=stdout, stderr=stderr, returncode=returncode) | ||
| + " (empty output)" | ||
| ) | ||
| return result |
There was a problem hiding this comment.
Misleading error message when stdout is empty on success
parse() delegates to explain_failure() even when returncode=0, so an empty-output success case produces the message "cursor agent exited with code 0. (empty output)". The phrase "exited with code 0" signals success to the reader while the surrounding context is an error, which is confusing. Consider a dedicated message when the exit code is 0 to avoid the contradiction.
| def _resolve_binary(self) -> str | None: | ||
| return resolve_cli_binary( | ||
| explicit_env_key="CURSOR_BIN", | ||
| binary_names=_candidate_binary_names("agent"), | ||
| fallback_paths=_default_cli_fallback_paths("agent"), | ||
| ) |
There was a problem hiding this comment.
Generic binary name
agent may resolve to an unrelated binary
_candidate_binary_names("agent") will match any executable called agent on the PATH (e.g. chef-agent, puppet-agent, or other common devops tools installed as agent). The detect() method only confirms the binary exits 0 on --version — it doesn't verify the output contains any Cursor-specific string. A false positive here sends the wrong binary to build() and fails at invocation time with a confusing error. Consider asserting that "cursor" appears in the --version output before treating the binary as the Cursor Agent CLI.
| if self.provider in ("ollama", "bedrock", "codex", "cursor"): | ||
| return self # ollama: local; bedrock: IAM; codex: `codex login` (CLI) |
There was a problem hiding this comment.
Inline comment doesn't mention
cursor
The comment still only describes ollama, bedrock, and codex even though cursor was added to the same exemption. This will mislead future readers.
| if self.provider in ("ollama", "bedrock", "codex", "cursor"): | |
| return self # ollama: local; bedrock: IAM; codex: `codex login` (CLI) | |
| if self.provider in ("ollama", "bedrock", "codex", "cursor"): | |
| return self # ollama: local; bedrock: IAM; codex/cursor: CLI login |
|
All P2 feedback addressed:
All tests passing, no behavior changes. |
| detail="Binary found but does not appear to be Cursor Agent CLI.", | ||
| ) | ||
|
|
||
| if os.environ.get("CURSOR_API_KEY"): |
There was a problem hiding this comment.
why this? the auth, models and everything should be handled from CLI side only and not something which should be configured here..
check in codex how its done https://github.com/Tracer-Cloud/opensre/blob/main/app/integrations/llm_cli/AGENTS.md#codex-env-quick-reference-instance-of-the-convention-above
and refer the above AGENTS.md the specs should strictly follow it.
There was a problem hiding this comment.
I've removed env-based auth handling and aligned the adapter with the CLI-only contract.
Updated tests accordingly to ensure authentication is determined solely via the CLI (agent status).
|
some points from acceptance criteria still missing. |
|
I've updated the PR description with a detailed acceptance criteria mapping and aligned the implementation with the CLI-only contract. The remaining Windows CI failures look unrelated to the Cursor adapter. Both failures are in interactive_shell prompt session tests and fail with prompt_toolkit.output.win32.NoConsoleScreenBufferError. The Cursor-specific tests are passing locally, and the full suite mostly passes except these Windows console-buffer failures. Let me know if anything else should be clarified @muddlebee 🙏 |
|
Hey @cerencamkiran resolve the conflicts over here |
| ) | ||
|
|
||
| status_text = (status_proc.stdout + status_proc.stderr).strip() | ||
| if status_proc.returncode == 0 and "Logged in as" in status_text: |
There was a problem hiding this comment.
Not-logged-in detail still mentions CURSOR_API_KEY while the adapter documents CLI-only auth elsewhere. Drop the env-var hint here for consistency.
| explicit_env_key="CURSOR_BIN", | ||
| binary_names=_candidate_binary_names("agent"), | ||
| fallback_paths=_default_cli_fallback_paths("agent"), | ||
| ) |
There was a problem hiding this comment.
Version-regex gating reduces wrong-binary risk but any agent that prints a semver can still pass. Consider requiring a Cursor-specific substring in --version output (case-insensitive "cursor") before treating the binary as installed.
| "bedrock", | ||
| "minimax", | ||
| "codex", | ||
| "cursor", |
There was a problem hiding this comment.
Unrelated to this line: the branch removes the repository root .gitignore entirely in the latest commit. Restore .gitignore before merge.
| adapter_factory=_codex_adapter_factory, | ||
| ), | ||
| ProviderOption( | ||
| value="cursor", |
There was a problem hiding this comment.
Wizard wires the provider; issue #1109 still expects .env.example entries for CURSOR_BIN and CURSOR_MODEL (mirroring CODEX). Add those commented lines in .env.example.
| logged_in = None | ||
| detail = status_text or "Could not determine Cursor Agent auth status." | ||
|
|
||
| return CLIProbe( |
There was a problem hiding this comment.
Classify subscription auth from agent status first (current block). If logged_in is False or None and CURSOR_API_KEY is non-empty, return logged_in True with detail that notes API-key headless auth; only surface agent login when neither path applies. Extend tests for not-logged-in status plus key, unclear status plus key, and logged-in status ignoring the key.
@cerencamkiran this is P1 issue |
|
Thanks for the feedback @muddlebee I’ve addressed the comments and aligned the auth handling with the current tests and CLI contract. All tests are passing locally and in CI, and I’ve resolved the remaining conflicts. |
|
@cerencamkiran why remove? .gitignore ? https://github.com/Tracer-Cloud/opensre/blob/main/.gitignore -- |
PR tip had an empty .gitignore; restore Tracer-Cloud main contents.
|
Looks good overall I will just do some additional set of reviews :) thank you for your patience and efforts 🪄 |
That was accidental while resolving a merge conflict. Thanks a lot for fixing it. 🤗 |
Happy to address any further feedback @muddlebee thanks again for the review 🙂 |
|
🎻 "The diff was clean, the tests did pass, the reviewer wept." That poem was about @cerencamkiran's PR. 🥹 👋 Join us on Discord - OpenSRE : hang out, contribute, or hunt for features and issues. Everyone's welcome. |
|
@cerencamkiran thank you for the PR. I added some improvements in later PRs. :) feel free to check |
Main moved while the previous merge was in flight: PR Tracer-Cloud#1149 added Cursor Agent CLI support touching the same prefix tuple, env-var docs, and provider-allowlist as this branch. Resolutions (all "combine both" merges): - subprocess_env.py: prefix tuple now includes both CURSOR_ and KIMI_ - app/config.py: provider early-return now includes both cursor and kimi - .env.example: keep both Cursor and Kimi env-var blocks








Fixes #1109
Summary
Adds support for Cursor Agent CLI as a subprocess-backed LLM provider using the existing
LLMCLIAdapterinterface.This integration enables OpenSRE to use Cursor as a non-interactive, one-shot CLI backend, fully compatible with the Codex-style execution model.
Key Features
agent --print--trustfor headless executionCURSOR_MODELagent login/agent status)subprocess.run(no TTY, no blocking)Spike Findings (Cursor CLI behavior)
agent(notcursor-agent)--print--trust)--model autoLLMCLIAdaptercontractImplementation
Added
CursorAdapterinapp/integrations/llm_cli/cursor.pyImplemented:
detect()(CLI-based status probing only)build()(non-interactive invocation)parse()(stdout extraction)explain_failure()(error classification)Registered provider in:
app/integrations/llm_cli/registry.pyapp/config.pyExtended subprocess env forwarding:
_SAFE_SUBPROCESS_ENV_PREFIXES += ("CURSOR_",)Added onboarding support in:
app/cli/wizard/config.pydetect() behavior
Returns:
True→ authenticatedFalse→ not authenticatedNone→ auth state unclearNever raises exceptions and follows the
CLIProbecontract.Tests
Added full test coverage:
Results:
Static checks:
Quick Start
agent login opensre onboard # Select: Cursor Agent CLI opensre investigateEnd-to-End Validation
Successfully verified full pipeline:
opensre onboard→ Cursor Agent CLI selectedopensre investigate→ completed successfullySample output:
Confirms Cursor CLI works end-to-end in OpenSRE investigation loop.
Demo (Headless Execution)
Cross-OS Notes
Tested on:
Notes:
agentbinaryAcceptance Criteria Coverage
Non-interactive CLI contract
agent --print)subprocess.rundetect() behavior (3-state)
True→ authenticated via CLI (agent status)False→ not authenticatedNone→ unclear / fallbackCLIProbecontractCLI-only authentication
agent status)build() contract
--print,--trust,--output-format textCURSOR_MODELSubprocess env forwarding
CURSOR_*variablesRegistry integration
CLI_PROVIDER_REGISTRYLLM_PROVIDER=cursorEnd-to-end validation
opensre onboardopensre investigateConclusion
Cursor CLI satisfies all requirements for a non-interactive LLM backend and integrates cleanly into the OpenSRE pipeline.