From de2be975d5e5430ffc17bd62e482c849ab43ee39 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 14:58:08 -0400 Subject: [PATCH 1/5] Add C# as a supported output language Follows the exact same pattern as python/javascript/typescript: - New prompt partial (prompts/partials/_language_csharp.md): uses System.Net.Http.HttpClient and System.Text.Json - both part of the .NET 5+ base class library, so no NuGet package is needed for HTTP or JSON (unlike Java, .NET's stdlib does include JSON support). Packaged as a minimal .csproj so `dotnet run` just works. Same auth-hardcoding/refresh guidance as the other languages. No filename vs. class-name naming workaround needed here (unlike Java) - C# doesn't require a public class's name to match its file's name. - base_engineer.py: added "csharp" to _OUTPUT_LANGUAGE_EXTENSIONS (.cs), _get_language_name() ("C#"), _get_run_command() ("dotnet run"), and _get_auto_output_files() (ApiClient.csproj, always included - a project file is required to `dotnet run` regardless of dependencies, matching typescript's "always" precedent rather than javascript/go's conditional one). - cli.py: added C# to the interactive `output_language` config picker. - Updated the three docstrings/comments that listed the language set (config.py, engineer.py, prompts/__init__.py) and README.md's output language line. - Tests: mirrored the existing per-language test pattern in test_base_engineer.py (extension, run command, prompt content) and test_prompts.py (partial loading) for C#. Checked the partial for the literal-brace str.format_map() bug caught in the Java PR before writing this one - no literal `{`/`}` anywhere in the new file (confirmed via grep), since C# code snippets weren't needed to convey the guidance here. Full test suite passes (49/49, run 3x) except the same pre-existing flaky test as the other language PRs (test_existing_client_language_falls_back_to_newest_file - a mtime-resolution race condition, unrelated to this change, present on a clean main checkout too). ruff check is clean on every file this touches (8 pre-existing lint errors elsewhere on main, none in the touched lines). --- CHANGELOG.md | 1 + README.md | 2 +- src/reverse_api/base_engineer.py | 5 +++++ src/reverse_api/cli.py | 1 + src/reverse_api/config.py | 2 +- src/reverse_api/engineer.py | 2 +- src/reverse_api/prompts/__init__.py | 2 +- .../prompts/partials/_language_csharp.md | 22 +++++++++++++++++++ tests/test_base_engineer.py | 13 +++++++++++ tests/test_prompts.py | 10 +++++++++ 10 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 src/reverse_api/prompts/partials/_language_csharp.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d691bd4..18106ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Go output language**: `output_language: "go"` is now supported alongside python/javascript/typescript, generating a standard-library-first (`net/http`, `encoding/json`) Go program, with the same auth-hardcoding/refresh and bot-detection-fallback guidance as the other languages. - **Java output language**: `output_language: "java"` is now supported alongside python/javascript/typescript, generating a small Maven project using `java.net.http.HttpClient` (JDK 11+, no HTTP library dependency) and Gson for JSON, with the same auth-hardcoding/refresh guidance as the other languages. +- **C# output language**: `output_language: "csharp"` is now supported alongside python/javascript/typescript, generating a minimal .NET project using `System.Net.Http.HttpClient` and `System.Text.Json` (both part of the .NET 5+ base class library — no NuGet dependency needed), with the same auth-hardcoding/refresh guidance as the other languages. ## [0.10.0] - 2026-06-01 diff --git a/README.md b/README.md index edd6e4d8..bacb16a5 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Settings live in `~/.reverse-api/config.json` and can be edited via `/settings` - **Models**: Sonnet 4.6 (default), Opus 4.6 (most capable), Haiku 4.5 (fastest). For OpenCode see [models.dev](https://models.dev). - **SDK**: `claude` (default), `opencode`, `cursor`, or `copilot` (GitHub Copilot). -- **Output language**: `python`, `javascript`, `typescript`, `go`, or `java`. +- **Output language**: `python`, `javascript`, `typescript`, `go`, `java`, or `csharp`. ## CLI diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index e93cc5b9..0e284b22 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -36,6 +36,7 @@ class BaseEngineer(ABC): "typescript": ".ts", "go": ".go", "java": ".java", + "csharp": ".cs", } def __init__( @@ -444,6 +445,7 @@ def _get_language_name(self) -> str: "typescript": "TypeScript", "go": "Go", "java": "Java", + "csharp": "C#", }.get(self.output_language, "Python") def _get_existing_client_guidance(self) -> str: @@ -494,6 +496,7 @@ def _get_run_command(self) -> str: "javascript": "node api_client.js", "typescript": "npx tsx api_client.ts", "go": "go run api_client.go", + "csharp": "dotnet run", }.get(self.output_language, "python api_client.py") def _get_codegen_instructions(self) -> str: @@ -610,6 +613,8 @@ def _get_auto_output_files(self, language_name: str, client_filename: str) -> st ) elif self.output_language == "java": return base + f"\n3. `{self.scripts_dir}/pom.xml` - Maven project file (Gson dependency, exec-maven-plugin)" + elif self.output_language == "csharp": + return base + f"\n3. `{self.scripts_dir}/ApiClient.csproj` - .NET project file" return base @abstractmethod diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index 5ee8a8f8..e6b32199 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -1085,6 +1085,7 @@ def handle_settings(mode_color=THEME_PRIMARY): Choice(title="typescript", value="typescript"), Choice(title="go", value="go"), Choice(title="java", value="java"), + Choice(title="csharp", value="csharp"), Choice(title="back", value="back"), ] lang = questionary.select( diff --git a/src/reverse_api/config.py b/src/reverse_api/config.py index 09d2d218..ad29c34b 100644 --- a/src/reverse_api/config.py +++ b/src/reverse_api/config.py @@ -20,7 +20,7 @@ "opencode_model": "claude-opus-4-6", "opencode_provider": "anthropic", "output_dir": None, # None means use ~/.reverse-api/runs - "output_language": "python", # "python", "javascript", "typescript", "go", or "java" + "output_language": "python", # "python", "javascript", "typescript", "go", "java", or "csharp" "real_time_sync": True, # Enable real-time file sync during engineering "sdk": "claude", # "claude", "opencode", "copilot", or "cursor" } diff --git a/src/reverse_api/engineer.py b/src/reverse_api/engineer.py index 7922d51a..b400c541 100644 --- a/src/reverse_api/engineer.py +++ b/src/reverse_api/engineer.py @@ -232,7 +232,7 @@ def run_reverse_engineering( cursor_setting_sources: Optional explicit list (overrides cursor_web_search), e.g. ["project","user","all"]. enable_sync: Enable real-time file syncing during engineering is_fresh: Whether to start fresh (ignore previous scripts) - output_language: Target language - "python", "javascript", "typescript", "go", or "java" + output_language: Target language - "python", "javascript", "typescript", "go", "java", or "csharp" output_mode: Output mode - "client" for API client code, "docs" for OpenAPI specification """ if sdk == "opencode": diff --git a/src/reverse_api/prompts/__init__.py b/src/reverse_api/prompts/__init__.py index 15833877..36ef4e04 100644 --- a/src/reverse_api/prompts/__init__.py +++ b/src/reverse_api/prompts/__init__.py @@ -54,7 +54,7 @@ def load_language_partial(language: str, **kwargs: str) -> str: """Load the language-specific codegen instructions partial. Args: - language: One of "python", "javascript", "typescript", "go", "java". + language: One of "python", "javascript", "typescript", "go", "java", "csharp". **kwargs: Placeholder values (scripts_dir, client_filename, run_command). """ return load(f"partials/_language_{language}", **kwargs) diff --git a/src/reverse_api/prompts/partials/_language_csharp.md b/src/reverse_api/prompts/partials/_language_csharp.md new file mode 100644 index 00000000..a0dc8d3e --- /dev/null +++ b/src/reverse_api/prompts/partials/_language_csharp.md @@ -0,0 +1,22 @@ +**Generate a C# program** that replicates the API calls found in the traffic. The following are guidelines — use your judgment on what's appropriate for the specific API: + +- Use `System.Net.Http.HttpClient` and `System.Text.Json` — both are part of the .NET base class library (.NET 5+), so no external NuGet package is needed for HTTP or JSON +- Create a minimal project file (`.csproj`, targeting `net8.0`) so the program can be run with a single command +- Create a separate method for each distinct API endpoint, with a small record or class for its response shape +- Reuse one `HttpClient` instance across requests rather than creating a new one per call +- Include a `Main` method with example usage + +**Authentication & credentials:** +- Hardcode all cookies, tokens, session IDs, and auth headers found in the traffic directly in the program +- The user should be able to run the program immediately with zero configuration — no env vars, no config files, no manual setup beyond what's generated +- If the API uses cookies, construct the `HttpClient` with an `HttpClientHandler` that has a `CookieContainer` set, so cookies persist across requests +- If the API uses Bearer tokens or API keys, hardcode them in the request headers (e.g. via `DefaultRequestHeaders`) +- Handle auth refresh so the program doesn't go stale: if you see a token refresh endpoint, OAuth refresh flow, or login endpoint in the traffic, implement automatic re-authentication when a request returns 401/403. If cookies have expiry, re-fetch them before they expire + +**Testing:** +- Run: `{run_command}` +- You have up to 5 attempts to fix issues + +Save the program to: `{scripts_dir}/{client_filename}` +Save documentation to: `{scripts_dir}/README.md` +Save the project file to: `{scripts_dir}/ApiClient.csproj` diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index cc4d1d24..06499d0b 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -202,6 +202,10 @@ def test_get_output_extension_java(self, tmp_path): """Java extension.""" eng = self._make_engineer(tmp_path, output_language="java") assert eng._get_output_extension() == ".java" + def test_get_output_extension_csharp(self, tmp_path): + """C# extension.""" + eng = self._make_engineer(tmp_path, output_language="csharp") + assert eng._get_output_extension() == ".cs" def test_get_output_extension_unknown(self, tmp_path): """Unknown language defaults to .py.""" @@ -269,6 +273,10 @@ def test_get_run_command_java_resolves_relative_output_dir(self, tmp_path): pom_arg = tokens[3] assert Path(pom_arg).is_absolute() assert pom_arg == str(eng.scripts_dir.resolve() / "pom.xml") + def test_get_run_command_csharp(self, tmp_path): + """Run command for C#.""" + eng = self._make_engineer(tmp_path, output_language="csharp") + assert eng._get_run_command() == "dotnet run" def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" @@ -327,6 +335,11 @@ def test_java_prompt(self, tmp_path): eng = self._make_engineer(tmp_path, output_language="java") system_prompt, user_message = eng._build_prompts() assert "Java program" in system_prompt + def test_csharp_prompt(self, tmp_path): + """C# prompt includes C#-specific instructions.""" + eng = self._make_engineer(tmp_path, output_language="csharp") + system_prompt, user_message = eng._build_prompts() + assert "C# program" in system_prompt assert "HttpClient" in system_prompt def test_docs_prompt(self, tmp_path): diff --git a/tests/test_prompts.py b/tests/test_prompts.py index 37f572a6..89fe391e 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -91,6 +91,16 @@ def test_java_partial(self): assert "Java program" in text assert "HttpClient" in text assert "/tmp/scripts/api_client.java" in text + def test_csharp_partial(self): + text = load_language_partial( + "csharp", + scripts_dir="/tmp/scripts", + client_filename="api_client.cs", + run_command="dotnet run", + ) + assert "C# program" in text + assert "HttpClient" in text + assert "/tmp/scripts/api_client.cs" in text class TestEngineerTemplates: From 3370d7611c6354a81915b4e9ed066add6332fce4 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 15:16:54 -0400 Subject: [PATCH 2/5] Fix working-directory and target-framework issues in C# support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real bugs caught by greptile-apps on the PR, both verified against the actual code before fixing: 1. Same class of bug as the Go and Java PRs: the agent's cwd for the whole session is scripts_dir.parent.parent (confirmed in engineer.py's analyze_and_generate -> ClaudeAgentOptions), not scripts_dir where ApiClient.csproj is saved. A bare `dotnet run` only looks for a project file in the current directory, so it wouldn't find it. _get_run_command() now points --project explicitly at this run's own .csproj path, the idiomatic dotnet-CLI way to do this (as opposed to Maven's -f flag used for the same fix in the Java PR). 2. The partial described HttpClient/System.Text.Json as available since .NET 5+, but then hardcoded the generated project's own to net8.0 unconditionally — a genuine internal contradiction: on a machine with only, say, .NET 6 installed, generation would succeed but the required dotnet run test step would fail with an unsupported-target-framework error, despite the partial's own text implying broader compatibility. Fixed by making the target framework adaptive: default to net8.0, but explicitly instructs lowering and retrying if the installed SDK doesn't support it - leaning on the same "test it, up to 5 attempts to fix issues" loop every partial already has, rather than assuming one hardcoded version always works. Updated test_get_run_command_csharp to match the new (scripts_dir- dependent) return value. A third bot finding (cli.py's `run --file` subcommand only discovers *.py scripts and always launches them with the shared venv's python interpreter) is the same pre-existing, cross-cutting gap already noted on the Java PR - confirmed discover_scripts() in utils.py has been Python-only since before javascript/typescript were added, so this isn't specific to C# either. Left out of scope here for the same reason. Full test suite passes (49/49, run 2x). ruff check clean on the touched lines (2 pre-existing unused-import warnings elsewhere in test_base_engineer.py, unrelated). --- src/reverse_api/base_engineer.py | 10 +++++++++- src/reverse_api/prompts/partials/_language_csharp.md | 4 ++-- tests/test_base_engineer.py | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 0e284b22..5fd07e9d 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -491,12 +491,20 @@ def _get_run_command(self) -> str: # requires ("symbolic reference class is not accessible"). pom = shlex.quote(str(self.scripts_dir.resolve() / "pom.xml")) return f"mvn -q -f {pom} compile exec:exec" + if self.output_language == "csharp": + # Unlike python/node/npx (which happily take a plain relative + # filename regardless of the agent's actual cwd, scripts_dir. + # parent.parent — see analyze_and_generate's ClaudeAgentOptions), + # a bare `dotnet run` only looks for a project file in the + # current directory. --project points it straight at this run's + # own .csproj regardless of cwd, rather than relying on the + # agent to cd there itself first. + return f'dotnet run --project "{self.scripts_dir}/ApiClient.csproj"' return { "python": "python api_client.py", "javascript": "node api_client.js", "typescript": "npx tsx api_client.ts", "go": "go run api_client.go", - "csharp": "dotnet run", }.get(self.output_language, "python api_client.py") def _get_codegen_instructions(self) -> str: diff --git a/src/reverse_api/prompts/partials/_language_csharp.md b/src/reverse_api/prompts/partials/_language_csharp.md index a0dc8d3e..6b589bf4 100644 --- a/src/reverse_api/prompts/partials/_language_csharp.md +++ b/src/reverse_api/prompts/partials/_language_csharp.md @@ -1,7 +1,7 @@ **Generate a C# program** that replicates the API calls found in the traffic. The following are guidelines — use your judgment on what's appropriate for the specific API: -- Use `System.Net.Http.HttpClient` and `System.Text.Json` — both are part of the .NET base class library (.NET 5+), so no external NuGet package is needed for HTTP or JSON -- Create a minimal project file (`.csproj`, targeting `net8.0`) so the program can be run with a single command +- Use `System.Net.Http.HttpClient` and `System.Text.Json` — both are part of the .NET base class library (available since .NET Core 3.0), so no external NuGet package is needed for HTTP or JSON +- Create a minimal project file (`.csproj`) so the program can be run with a single command. Target `net8.0` by default, but this only works if that SDK is actually installed — if `dotnet run` reports the target framework isn't supported, lower `` to whatever the installed SDK does support (e.g. `net6.0`) and retry; both libraries above work fine on any of these versions, only the project file's stated target needs to match what's actually installed - Create a separate method for each distinct API endpoint, with a small record or class for its response shape - Reuse one `HttpClient` instance across requests rather than creating a new one per call - Include a `Main` method with example usage diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 06499d0b..25d272ec 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -274,9 +274,12 @@ def test_get_run_command_java_resolves_relative_output_dir(self, tmp_path): assert Path(pom_arg).is_absolute() assert pom_arg == str(eng.scripts_dir.resolve() / "pom.xml") def test_get_run_command_csharp(self, tmp_path): - """Run command for C#.""" + """Run command for C# points --project at this run's own .csproj, + not a bare `dotnet run` — the agent's cwd is scripts_dir.parent. + parent (see analyze_and_generate), and dotnet only looks for a + project file in the current directory.""" eng = self._make_engineer(tmp_path, output_language="csharp") - assert eng._get_run_command() == "dotnet run" + assert eng._get_run_command() == f'dotnet run --project "{eng.scripts_dir}/ApiClient.csproj"' def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From 93399b8433471669b517c84b053b2bb715d406d7 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 15:50:12 -0400 Subject: [PATCH 3/5] Fix shell-quoting in the C# run command Same class of bug caught by review bots on the PHP PR, applied here proactively: _get_run_command()'s "--project \"{path}\"" only looked safe - manual double-quoting doesn't stop $()/backtick command substitution inside a double-quoted shell string. Replaced with shlex.quote(). Added a test with a deliberately hostile path (embedded $(rm -rf ~)) asserting the command round-trips through shlex.split() back to the literal path. Full test suite passes (50/50, run 2x) except the same pre-existing flaky test noted on the other language PRs. --- src/reverse_api/base_engineer.py | 8 ++++++-- tests/test_base_engineer.py | 21 ++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 5fd07e9d..0c2f229f 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -498,8 +498,12 @@ def _get_run_command(self) -> str: # a bare `dotnet run` only looks for a project file in the # current directory. --project points it straight at this run's # own .csproj regardless of cwd, rather than relying on the - # agent to cd there itself first. - return f'dotnet run --project "{self.scripts_dir}/ApiClient.csproj"' + # agent to cd there itself first. shlex.quote(), not manual + # double-quoting — output_dir (and so scripts_dir) isn't + # guaranteed free of shell metacharacters, and naive f'"{path}"' + # still lets $()/backticks expand inside double quotes. + csproj = shlex.quote(f"{self.scripts_dir}/ApiClient.csproj") + return f"dotnet run --project {csproj}" return { "python": "python api_client.py", "javascript": "node api_client.js", diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 25d272ec..838fa4ec 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -274,12 +274,23 @@ def test_get_run_command_java_resolves_relative_output_dir(self, tmp_path): assert Path(pom_arg).is_absolute() assert pom_arg == str(eng.scripts_dir.resolve() / "pom.xml") def test_get_run_command_csharp(self, tmp_path): - """Run command for C# points --project at this run's own .csproj, - not a bare `dotnet run` — the agent's cwd is scripts_dir.parent. - parent (see analyze_and_generate), and dotnet only looks for a - project file in the current directory.""" + """Run command for C# points --project at this run's own (shell- + quoted) .csproj, not a bare `dotnet run` — the agent's cwd is + scripts_dir.parent.parent (see analyze_and_generate), and dotnet + only looks for a project file in the current directory.""" eng = self._make_engineer(tmp_path, output_language="csharp") - assert eng._get_run_command() == f'dotnet run --project "{eng.scripts_dir}/ApiClient.csproj"' + expected_csproj = shlex.quote(f"{eng.scripts_dir}/ApiClient.csproj") + assert eng._get_run_command() == f"dotnet run --project {expected_csproj}" + + def test_get_run_command_csharp_quotes_metacharacters(self, tmp_path): + """A scripts_dir containing shell metacharacters must round-trip + back to the literal path, not be left open to $()/backtick + expansion — what the naive f'"{path}"' approach got wrong.""" + eng = self._make_engineer(tmp_path, output_language="csharp") + eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir") + tokens = shlex.split(eng._get_run_command()) + assert tokens[:2] == ["dotnet", "run"] + assert tokens[3] == f"{eng.scripts_dir}/ApiClient.csproj" def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From 404f03e654376bcd21e765462b708df64b71efc9 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 16:58:56 -0400 Subject: [PATCH 4/5] Resolve scripts_dir to an absolute path in the C# run command cubic flagged that a relative --output-dir breaks --project: scripts_dir stays relative to the original cwd, but once the agent's cwd moves to scripts_dir.parent.parent (see analyze_and_generate), embedding that same relative string re-interprets it from the new location and points at a doubly-nested, nonexistent path. Resolve scripts_dir before interpolating it so the command is genuinely cwd-independent, matching the intent already documented in the surrounding comment. --- src/reverse_api/base_engineer.py | 6 +++++- tests/test_base_engineer.py | 21 +++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 0c2f229f..7e7d85d9 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -502,7 +502,11 @@ def _get_run_command(self) -> str: # double-quoting — output_dir (and so scripts_dir) isn't # guaranteed free of shell metacharacters, and naive f'"{path}"' # still lets $()/backticks expand inside double quotes. - csproj = shlex.quote(f"{self.scripts_dir}/ApiClient.csproj") + # .resolve(): a relative --output-dir would otherwise be + # re-interpreted against the agent's cwd (scripts_dir.parent. + # parent) instead of the original cwd it was relative to, + # pointing --project at the wrong, doubly-nested location. + csproj = shlex.quote(str(self.scripts_dir.resolve() / "ApiClient.csproj")) return f"dotnet run --project {csproj}" return { "python": "python api_client.py", diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 838fa4ec..774b66a7 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -274,12 +274,12 @@ def test_get_run_command_java_resolves_relative_output_dir(self, tmp_path): assert Path(pom_arg).is_absolute() assert pom_arg == str(eng.scripts_dir.resolve() / "pom.xml") def test_get_run_command_csharp(self, tmp_path): - """Run command for C# points --project at this run's own (shell- - quoted) .csproj, not a bare `dotnet run` — the agent's cwd is + """Run command for C# points --project at this run's own (resolved, + shell-quoted) .csproj, not a bare `dotnet run` — the agent's cwd is scripts_dir.parent.parent (see analyze_and_generate), and dotnet only looks for a project file in the current directory.""" eng = self._make_engineer(tmp_path, output_language="csharp") - expected_csproj = shlex.quote(f"{eng.scripts_dir}/ApiClient.csproj") + expected_csproj = shlex.quote(str(eng.scripts_dir.resolve() / "ApiClient.csproj")) assert eng._get_run_command() == f"dotnet run --project {expected_csproj}" def test_get_run_command_csharp_quotes_metacharacters(self, tmp_path): @@ -290,7 +290,20 @@ def test_get_run_command_csharp_quotes_metacharacters(self, tmp_path): eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir") tokens = shlex.split(eng._get_run_command()) assert tokens[:2] == ["dotnet", "run"] - assert tokens[3] == f"{eng.scripts_dir}/ApiClient.csproj" + assert tokens[3] == str(eng.scripts_dir.resolve() / "ApiClient.csproj") + + def test_get_run_command_csharp_resolves_relative_output_dir(self, tmp_path): + """A relative scripts_dir must be resolved to an absolute path before + being embedded in the command — otherwise, once the agent's cwd + moves to scripts_dir.parent.parent, the same relative string gets + re-interpreted from there and points at the wrong, doubly-nested + location.""" + eng = self._make_engineer(tmp_path, output_language="csharp") + eng.scripts_dir = Path("relative_output/scripts/run123") + tokens = shlex.split(eng._get_run_command()) + project_arg = tokens[3] + assert Path(project_arg).is_absolute() + assert project_arg == str(eng.scripts_dir.resolve() / "ApiClient.csproj") def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From 6306e3978ed66c6830254f2e92003332e4a65fec Mon Sep 17 00:00:00 2001 From: kalil0321 Date: Wed, 22 Jul 2026 12:56:08 +0200 Subject: [PATCH 5/5] Make the C# target-framework guidance direction-neutral MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The partial told the agent to *lower* TargetFramework when dotnet run rejects it, but a machine with only a newer SDK (e.g. .NET 10, no .NET 8 runtime) needs the target *raised* — net8.0 builds fine under SDK 10 but fails at launch. Observed in live testing: the agent recovered anyway from dotnet's error message, but the guidance shouldn't point the wrong way. --- src/reverse_api/prompts/partials/_language_csharp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reverse_api/prompts/partials/_language_csharp.md b/src/reverse_api/prompts/partials/_language_csharp.md index 6b589bf4..120e6b24 100644 --- a/src/reverse_api/prompts/partials/_language_csharp.md +++ b/src/reverse_api/prompts/partials/_language_csharp.md @@ -1,7 +1,7 @@ **Generate a C# program** that replicates the API calls found in the traffic. The following are guidelines — use your judgment on what's appropriate for the specific API: - Use `System.Net.Http.HttpClient` and `System.Text.Json` — both are part of the .NET base class library (available since .NET Core 3.0), so no external NuGet package is needed for HTTP or JSON -- Create a minimal project file (`.csproj`) so the program can be run with a single command. Target `net8.0` by default, but this only works if that SDK is actually installed — if `dotnet run` reports the target framework isn't supported, lower `` to whatever the installed SDK does support (e.g. `net6.0`) and retry; both libraries above work fine on any of these versions, only the project file's stated target needs to match what's actually installed +- Create a minimal project file (`.csproj`) so the program can be run with a single command. Target `net8.0` by default, but this only works if that SDK/runtime is actually installed — if `dotnet run` reports the target framework isn't supported or the required runtime isn't installed, change `` to match what's actually there (check `dotnet --version`; a machine with only a newer SDK needs a higher target like `net10.0`, an older one a lower target like `net6.0`) and retry; both libraries above work fine on any of these versions, only the project file's stated target needs to match the installed SDK - Create a separate method for each distinct API endpoint, with a small record or class for its response shape - Reuse one `HttpClient` instance across requests rather than creating a new one per call - Include a `Main` method with example usage