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 @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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.
- **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.

## [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`, or `csharp`.
- **Output language**: `python`, `javascript`, `typescript`, `go`, `java`, `csharp`, or `php`.

## CLI

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

def __init__(
Expand Down Expand Up @@ -446,6 +447,7 @@ def _get_language_name(self) -> str:
"go": "Go",
"java": "Java",
"csharp": "C#",
"php": "PHP",
}.get(self.output_language, "Python")

def _get_existing_client_guidance(self) -> str:
Expand Down Expand Up @@ -508,6 +510,26 @@ def _get_run_command(self) -> str:
# pointing --project at the wrong, doubly-nested location.
csproj = shlex.quote(str(self.scripts_dir.resolve() / "ApiClient.csproj"))
return f"dotnet run --project {csproj}"
if self.output_language == "php":
# Full path, not a bare relative "php api_client.php": the
# agent's cwd for the whole session is scripts_dir.parent.parent
# (see analyze_and_generate's ClaudeAgentOptions), not
# scripts_dir itself where the script is actually saved. python/
# node/npx get away with a bare relative filename here since
# this is already how they're shipped and evidently work in
# practice, but there's no reason to leave a new language
Comment thread
greptile-apps[bot] marked this conversation as resolved.
# exposed to the same ambiguity when it's this cheap to remove.
# 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 (confirmed live: shlex.quote handles this, plain
# double-quoting doesn't).
# .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 this command at the wrong, doubly-nested location.
path = shlex.quote(str(self.scripts_dir.resolve() / self._get_client_filename()))
return f"php {path}"
return {
"python": "python api_client.py",
"javascript": "node api_client.js",
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 @@ -1086,6 +1086,7 @@ def handle_settings(mode_color=THEME_PRIMARY):
Choice(title="go", value="go"),
Choice(title="java", value="java"),
Choice(title="csharp", value="csharp"),
Choice(title="php", value="php"),
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", or "csharp"
"output_language": "python", # "python", "javascript", "typescript", "go", "java", "csharp", or "php"
"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", or "csharp"
output_language: Target language - "python", "javascript", "typescript", "go", "java", "csharp", or "php"
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".
language: One of "python", "javascript", "typescript", "go", "java", "csharp", "php".
**kwargs: Placeholder values (scripts_dir, client_filename, run_command).
"""
return load(f"partials/_language_{language}", **kwargs)
Expand Down
21 changes: 21 additions & 0 deletions src/reverse_api/prompts/partials/_language_php.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
**Generate a PHP script** 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 the `curl` extension (`curl_init`/`curl_exec`) for requests and `json_encode`/`json_decode` for JSON — no Composer dependency is needed for either. `ext-json` is bundled with PHP core and always available, but `ext-curl` is bundled-but-optional and isn't guaranteed enabled on every install — if `curl_init` isn't defined, install it with whatever package manager is available (e.g. `apt-get install -y php-curl`) before retrying
- Reuse one cURL handle across requests rather than creating a new one per call
- Create a separate function for each distinct API endpoint
- Include type declarations on function signatures where they add clarity
- Include example usage at the bottom of the script

**Authentication & credentials:**
- Hardcode all cookies, tokens, session IDs, and auth headers found in the traffic directly in the script
- The user should be able to run the script immediately with zero configuration — no env vars, no config files, no `composer install`
- If the API uses cookies, configure the cURL handle's cookie jar (`CURLOPT_COOKIEJAR`/`CURLOPT_COOKIEFILE`) so cookies persist automatically across requests
- If the API uses Bearer tokens or API keys, hardcode them in the request headers
- Handle auth refresh so the script 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 script to: `{scripts_dir}/{client_filename}`
Save documentation to: `{scripts_dir}/README.md`
45 changes: 45 additions & 0 deletions tests/test_base_engineer.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ 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_php(self, tmp_path):
"""PHP extension."""
eng = self._make_engineer(tmp_path, output_language="php")
assert eng._get_output_extension() == ".php"

def test_get_output_extension_unknown(self, tmp_path):
"""Unknown language defaults to .py."""
Expand Down Expand Up @@ -304,6 +308,41 @@ def test_get_run_command_csharp_resolves_relative_output_dir(self, tmp_path):
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_php(self, tmp_path):
"""Run command for PHP uses the full, resolved, shell-quoted path,
not a bare relative filename — the agent's cwd is scripts_dir.
parent.parent (see analyze_and_generate), not scripts_dir where the
script lives, and shlex.quote() (not manual double-quoting) is what
actually neutralizes shell metacharacters in an arbitrary output_dir."""
eng = self._make_engineer(tmp_path, output_language="php")
expected_path = shlex.quote(str(eng.scripts_dir.resolve() / "api_client.php"))
assert eng._get_run_command() == f"php {expected_path}"

def test_get_run_command_php_quotes_metacharacters(self, tmp_path):
"""A scripts_dir containing shell metacharacters must round-trip
back to the literal path when the shell tokenizes the command —
not be left open to $()/backtick expansion, which is exactly what
the naive f'"{path}"' approach got wrong (double quotes still allow
command substitution inside them)."""
eng = self._make_engineer(tmp_path, output_language="php")
eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir")
command = eng._get_run_command()
tokens = shlex.split(command)
assert tokens[0] == "php"
assert tokens[1] == str(eng.scripts_dir.resolve() / "api_client.php")

def test_get_run_command_php_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="php")
eng.scripts_dir = Path("relative_output/scripts/run123")
tokens = shlex.split(eng._get_run_command())
script_arg = tokens[1]
assert Path(script_arg).is_absolute()
assert script_arg == str(eng.scripts_dir.resolve() / "api_client.php")

def test_get_run_command_unknown(self, tmp_path):
"""Unknown language defaults to Python command."""
Expand Down Expand Up @@ -368,6 +407,12 @@ def test_csharp_prompt(self, tmp_path):
system_prompt, user_message = eng._build_prompts()
assert "C# program" in system_prompt
assert "HttpClient" in system_prompt
def test_php_prompt(self, tmp_path):
"""PHP prompt includes PHP-specific instructions."""
eng = self._make_engineer(tmp_path, output_language="php")
system_prompt, user_message = eng._build_prompts()
assert "PHP script" in system_prompt
assert "curl" 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 @@ -101,6 +101,16 @@ def test_csharp_partial(self):
assert "C# program" in text
assert "HttpClient" in text
assert "/tmp/scripts/api_client.cs" in text
def test_php_partial(self):
text = load_language_partial(
"php",
scripts_dir="/tmp/scripts",
client_filename="api_client.php",
run_command='php "/tmp/scripts/api_client.php"',
)
assert "PHP script" in text
assert "curl" in text
assert "/tmp/scripts/api_client.php" in text


class TestEngineerTemplates:
Expand Down