Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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.
- **PHP output language**: `output_language: "php"` is now supported alongside python/javascript/typescript, generating a script using the `curl` and `json_encode`/`json_decode` core extensions (`ext-curl`, `ext-json` — no Composer dependency needed), with the same auth-hardcoding/refresh guidance as the other languages.
- **Ruby output language**: `output_language: "ruby"` is now supported alongside python/javascript/typescript, generating a script using `net/http` and `json` (both part of Ruby's standard library — no gem/Bundler dependency needed), with the same auth-hardcoding/refresh guidance as the other languages.
- **C output language**: `output_language: "c"` is now supported alongside python/javascript/typescript, generating a program using `libcurl` for HTTP and a vendored `cJSON` for JSON (C has neither in its standard library), compiled and run as a single `{run_command}` step, with the same auth-hardcoding/refresh guidance as the other languages.

## [0.10.0] - 2026-06-01

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`, `java`, `csharp`, `php`, or `ruby`.
- **Output language**: `python`, `javascript`, `typescript`, `go`, `java`, `csharp`, `php`, `ruby`, or `c`. C needs a POSIX toolchain (`cc`, libcurl headers) — macOS/Linux, or WSL/MSYS2 on Windows.

## CLI

Expand Down
25 changes: 25 additions & 0 deletions src/reverse_api/base_engineer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class BaseEngineer(ABC):
"csharp": ".cs",
"php": ".php",
"ruby": ".rb",
"c": ".c",
}

def __init__(
Expand Down Expand Up @@ -450,6 +451,7 @@ def _get_language_name(self) -> str:
"csharp": "C#",
"php": "PHP",
"ruby": "Ruby",
"c": "C",
}.get(self.output_language, "Python")

def _get_existing_client_guidance(self) -> str:
Expand Down Expand Up @@ -546,6 +548,24 @@ def _get_run_command(self) -> str:
# pointing this command at the wrong, doubly-nested location.
path = shlex.quote(str(self.scripts_dir.resolve() / self._get_client_filename()))
return f"ruby {path}"
if self.output_language == "c":
# Unlike every other language here, C needs an explicit compile
# step before it can run at all — one shell command chains
# both. Full paths throughout, not bare relative filenames: the
# agent's cwd for the whole session is scripts_dir.parent.parent
# (see analyze_and_generate's ClaudeAgentOptions), not
# scripts_dir where the source/output actually live.
# shlex.quote (not manual double-quoting) so shell metacharacters
# in any of these three paths can't be interpreted as command
# substitution. .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 all three at the wrong, doubly-nested location.
resolved = self.scripts_dir.resolve()
source = shlex.quote(str(resolved / self._get_client_filename()))
cjson = shlex.quote(str(resolved / "cJSON.c"))
binary = shlex.quote(str(resolved / "api_client"))
return f"cc {source} {cjson} -lcurl -o {binary} && {binary}"
return {
"python": "python api_client.py",
"javascript": "node api_client.js",
Expand Down Expand Up @@ -669,6 +689,11 @@ def _get_auto_output_files(self, language_name: str, client_filename: str) -> st
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"
elif self.output_language == "c":
return base + (
f"\n3. `{self.scripts_dir}/cJSON.c` and `{self.scripts_dir}/cJSON.h` - "
"Vendored JSON library"
)
return base

@abstractmethod
Expand Down
1 change: 1 addition & 0 deletions src/reverse_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,7 @@ def handle_settings(mode_color=THEME_PRIMARY):
Choice(title="csharp", value="csharp"),
Choice(title="php", value="php"),
Choice(title="ruby", value="ruby"),
Choice(title="c", value="c"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 C Runs Are Not Discoverable

Selecting C creates api_client.c, but reverse-api-engineer run <run_id> still discovers only files with a .py suffix. A C-only run is therefore reported as having no scripts, so the normal run-by-ID workflow cannot execute the newly supported output.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/reverse_api/cli.py
Line: 1086

Comment:
**C Runs Are Not Discoverable**

Selecting C creates `api_client.c`, but `reverse-api-engineer run <run_id>` still discovers only files with a `.py` suffix. A C-only run is therefore reported as having no scripts, so the normal run-by-ID workflow cannot execute the newly supported output.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Adding c to the output-language picker exposes a path where reverse-api-engineer run <run_id> can’t execute C-only outputs if run discovery still filters to .py files. It would help to wire C into run-file discovery so the standard run-by-ID workflow works for the new language.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/reverse_api/cli.py, line 1086:

<comment>Adding `c` to the output-language picker exposes a path where `reverse-api-engineer run <run_id>` can’t execute C-only outputs if run discovery still filters to `.py` files. It would help to wire C into run-file discovery so the standard run-by-ID workflow works for the new language.</comment>

<file context>
@@ -1083,6 +1083,7 @@ def handle_settings(mode_color=THEME_PRIMARY):
             Choice(title="python", value="python"),
             Choice(title="javascript", value="javascript"),
             Choice(title="typescript", value="typescript"),
+            Choice(title="c", value="c"),
             Choice(title="back", value="back"),
         ]
</file context>

Choice(title="back", value="back"),
]
lang = questionary.select(
Expand Down
2 changes: 1 addition & 1 deletion src/reverse_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "java", "csharp", "php", or "ruby"
"output_language": "python", # "python", "javascript", "typescript", "go", "java", "csharp", "php", "ruby", or "c"
"real_time_sync": True, # Enable real-time file sync during engineering
"sdk": "claude", # "claude", "opencode", "copilot", or "cursor"
}
Expand Down
2 changes: 1 addition & 1 deletion src/reverse_api/engineer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "java", "csharp", "php", or "ruby"
output_language: Target language - "python", "javascript", "typescript", "go", "java", "csharp", "php", "ruby", or "c"
output_mode: Output mode - "client" for API client code, "docs" for OpenAPI specification
"""
if sdk == "opencode":
Expand Down
2 changes: 1 addition & 1 deletion src/reverse_api/prompts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "csharp", "php", "ruby".
language: One of "python", "javascript", "typescript", "go", "java", "csharp", "php", "ruby", "c".
**kwargs: Placeholder values (scripts_dir, client_filename, run_command).
"""
return load(f"partials/_language_{language}", **kwargs)
Expand Down
26 changes: 26 additions & 0 deletions src/reverse_api/prompts/partials/_language_c.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
**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:

- C has no HTTP client or JSON support in its standard library, unlike every other output language here — use `libcurl` for requests and vendor `cJSON` (a small, widely-used, permissively-licensed single-file JSON library) for JSON, rather than hand-rolling either from scratch
- Don't hand-write or reconstruct `cJSON.c`/`cJSON.h` from memory — fetch the exact, pinned upstream source instead, so the vendored copy is the real, complete library rather than a possibly incomplete or subtly wrong approximation:
- `curl -fsSL -o {scripts_dir}/cJSON.c https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.18/cJSON.c`
- `curl -fsSL -o {scripts_dir}/cJSON.h https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.18/cJSON.h`
- `libcurl` itself is a system library this project can't fetch or vendor — if compilation fails because `curl/curl.h` isn't found, stop and report a clear error naming the missing prerequisite (e.g. "libcurl development headers not found — install libcurl4-openssl-dev (Debian/Ubuntu) or curl (Homebrew) and retry") rather than running a package manager yourself; installing system packages is a host change the user should make and confirm, not something to do silently inside an auto-authorized session
- Reuse one `CURL` handle across requests rather than creating a new one per call
- Create a separate function for each distinct API endpoint, with a small struct for its response shape
- Check every `libcurl`/allocation return value; don't ignore errors
- Include a `main` function 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, enable `libcurl`'s cookie engine (`CURLOPT_COOKIEFILE`, e.g. pointed at an in-memory/empty string to just turn it on) so cookies persist automatically across requests on the same handle
- If the API uses Bearer tokens or API keys, hardcode them in the request headers
- 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:**
- Unlike every other language here, this needs a compile step before it can run — a single `{run_command}` handles both
- 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 vendored JSON library to: `{scripts_dir}/cJSON.c` and `{scripts_dir}/cJSON.h`
54 changes: 54 additions & 0 deletions tests/test_base_engineer.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ def test_get_output_extension_ruby(self, tmp_path):
"""Ruby extension."""
eng = self._make_engineer(tmp_path, output_language="ruby")
assert eng._get_output_extension() == ".rb"
def test_get_output_extension_c(self, tmp_path):
"""C extension."""
eng = self._make_engineer(tmp_path, output_language="c")
assert eng._get_output_extension() == ".c"

def test_get_output_extension_unknown(self, tmp_path):
"""Unknown language defaults to .py."""
Expand Down Expand Up @@ -378,6 +382,50 @@ def test_get_run_command_ruby_resolves_relative_output_dir(self, tmp_path):
script_arg = tokens[1]
assert Path(script_arg).is_absolute()
assert script_arg == str(eng.scripts_dir.resolve() / "api_client.rb")
def test_get_run_command_c(self, tmp_path):
"""Run command for C compiles and runs as one step, using full,
resolved, shell-quoted paths throughout — the agent's cwd is
scripts_dir.parent.parent (see analyze_and_generate), not
scripts_dir where the source, vendored cJSON, and compiled binary
all actually live."""
eng = self._make_engineer(tmp_path, output_language="c")
resolved = eng.scripts_dir.resolve()
source = shlex.quote(str(resolved / "api_client.c"))
cjson = shlex.quote(str(resolved / "cJSON.c"))
binary = shlex.quote(str(resolved / "api_client"))
expected = f"cc {source} {cjson} -lcurl -o {binary} && {binary}"
assert eng._get_run_command() == expected

def test_get_run_command_c_quotes_metacharacters(self, tmp_path):
"""A scripts_dir containing shell metacharacters must round-trip
back to the literal path for all three paths (source, cJSON,
binary), not be left open to $()/backtick expansion — what the
naive f'"{path}"' approach got wrong."""
eng = self._make_engineer(tmp_path, output_language="c")
eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir")
resolved = eng.scripts_dir.resolve()
tokens = shlex.split(eng._get_run_command())
assert tokens[:2] == ["cc", str(resolved / "api_client.c")]
assert tokens[2] == str(resolved / "cJSON.c")
assert tokens[3:6] == ["-lcurl", "-o", str(resolved / "api_client")]
assert tokens[6] == "&&"
assert tokens[7] == str(resolved / "api_client")

def test_get_run_command_c_resolves_relative_output_dir(self, tmp_path):
"""A relative scripts_dir must be resolved to an absolute path for
all three paths (source, cJSON, binary) 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="c")
eng.scripts_dir = Path("relative_output/scripts/run123")
resolved = eng.scripts_dir.resolve()
tokens = shlex.split(eng._get_run_command())
assert Path(tokens[1]).is_absolute()
assert tokens[1] == str(resolved / "api_client.c")
assert tokens[2] == str(resolved / "cJSON.c")
assert tokens[5] == str(resolved / "api_client")

def test_get_run_command_unknown(self, tmp_path):
"""Unknown language defaults to Python command."""
Expand Down Expand Up @@ -454,6 +502,12 @@ def test_ruby_prompt(self, tmp_path):
system_prompt, user_message = eng._build_prompts()
assert "Ruby script" in system_prompt
assert "net/http" in system_prompt
def test_c_prompt(self, tmp_path):
"""C prompt includes C-specific instructions."""
eng = self._make_engineer(tmp_path, output_language="c")
system_prompt, user_message = eng._build_prompts()
assert "C program" in system_prompt
assert "libcurl" in system_prompt

def test_docs_prompt(self, tmp_path):
"""Docs mode prompt includes OpenAPI instructions."""
Expand Down
10 changes: 10 additions & 0 deletions tests/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ def test_ruby_partial(self):
assert "Ruby script" in text
assert "net/http" in text
assert "/tmp/scripts/api_client.rb" in text
def test_c_partial(self):
text = load_language_partial(
"c",
scripts_dir="/tmp/scripts",
client_filename="api_client.c",
run_command='cc "/tmp/scripts/api_client.c" "/tmp/scripts/cJSON.c" -lcurl -o "/tmp/scripts/api_client" && "/tmp/scripts/api_client"',
)
assert "C program" in text
assert "libcurl" in text
assert "/tmp/scripts/api_client.c" in text


class TestEngineerTemplates:
Expand Down