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 @@ -9,6 +9,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.

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

## CLI

Expand Down
26 changes: 26 additions & 0 deletions src/reverse_api/base_engineer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import os
import shlex
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -34,6 +35,7 @@ class BaseEngineer(ABC):
"javascript": ".js",
"typescript": ".ts",
"go": ".go",
"java": ".java",
}

def __init__(
Expand Down Expand Up @@ -441,6 +443,7 @@ def _get_language_name(self) -> str:
"javascript": "JavaScript",
"typescript": "TypeScript",
"go": "Go",
"java": "Java",
}.get(self.output_language, "Python")

def _get_existing_client_guidance(self) -> str:
Expand All @@ -465,6 +468,27 @@ def _get_client_filename(self) -> str:

def _get_run_command(self) -> str:
"""Return the command to run the generated client."""
if self.output_language == "java":
# 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),
# Maven hard-fails immediately if invoked from a directory with
# no pom.xml, no upward search. -f points it straight at the
# right project file regardless of cwd, rather than relying on
# the 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.
# .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 -f at the wrong, doubly-nested location.
# exec:exec (spawn a real java process), not exec:java —
# exec:java invokes main() reflectively in-process, which fails
# on the package-private ApiClient class the Java partial
# 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"
return {
"python": "python api_client.py",
"javascript": "node api_client.js",
Expand Down Expand Up @@ -584,6 +608,8 @@ def _get_auto_output_files(self, language_name: str, client_filename: str) -> st
f"\n3. `{self.scripts_dir}/go.mod` and `{self.scripts_dir}/go.sum` - "
"Only if external dependencies are needed"
)
elif self.output_language == "java":
return base + f"\n3. `{self.scripts_dir}/pom.xml` - Maven project file (Gson dependency, exec-maven-plugin)"
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 @@ -1084,6 +1084,7 @@ def handle_settings(mode_color=THEME_PRIMARY):
Choice(title="javascript", value="javascript"),
Choice(title="typescript", value="typescript"),
Choice(title="go", value="go"),
Choice(title="java", value="java"),

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 Java Runs Remain Python-Only

Selecting Java creates api_client.java, but the existing run workflow discovers only .py files and launches selected files with the virtualenv Python interpreter. A Java run is therefore omitted from run --ls, reports that no Python scripts exist, or produces a Python syntax error when passed explicitly.

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

Comment:
**Java Runs Remain Python-Only**

Selecting Java creates `api_client.java`, but the existing `run` workflow discovers only `.py` files and launches selected files with the virtualenv Python interpreter. A Java run is therefore omitted from `run --ls`, reports that no Python scripts exist, or produces a Python syntax error when passed explicitly.

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

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", or "go"
"output_language": "python", # "python", "javascript", "typescript", "go", or "java"
"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", or "go"
output_language: Target language - "python", "javascript", "typescript", "go", or "java"
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".
language: One of "python", "javascript", "typescript", "go", "java".
**kwargs: Placeholder values (scripts_dir, client_filename, run_command).
"""
return load(f"partials/_language_{language}", **kwargs)
Expand Down
41 changes: 41 additions & 0 deletions src/reverse_api/prompts/partials/_language_java.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
**Generate a Java 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 `java.net.http.HttpClient` (built into the JDK since 11) for requests — no HTTP library dependency needed
- Use Gson for JSON parsing/serialization — the one dependency this needs, since the JDK has no built-in JSON support
- Create a minimal Maven project (`pom.xml`) with the Gson dependency and the `exec-maven-plugin`, so the client runs with a single command and no extra flags. A conventional Maven layout only compiles `src/main/java`, but `{client_filename}` is saved at the project root (see below) — override the build's `<sourceDirectory>` to `.` (project root) in the POM so `mvn compile` actually finds and compiles it
- Configure `exec-maven-plugin` for the `exec:exec` goal with exactly this configuration — do not use the `exec:java` goal or `<mainClass>`: `exec:java` invokes `main` reflectively in-process, which fails on the package-private `ApiClient` class described below ("symbolic reference class is not accessible"), while `exec:exec` spawns a real `java` process, and its `<classpath/>` element expands to the full dependency classpath with the correct platform-specific separator (`:` on macOS/Linux, `;` on Windows):

```xml
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>ApiClient</argument>
</arguments>
</configuration>
</plugin>
```
- Create a separate method for each distinct API endpoint, with a small 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
- The output file is named `{client_filename}` (lowercase with underscores), so name the top-level class holding `main` exactly `ApiClient`, declared package-private, *without* the `public` modifier — a `public` class's filename must exactly match its class name, and this generator's file naming convention doesn't follow Java's usual PascalCase file naming. A package-private top-level class compiles and runs identically; it just isn't visible from other packages, which this single-file client has no need for.

**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 after `mvn compile` — no env vars, no additional config files, no manual setup beyond what's generated
- If the API uses cookies, build the `HttpClient` with a `CookieHandler`/`CookieManager` so cookies persist across requests
- 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:**
- Run: `{run_command}`
Comment thread
greptile-apps[bot] marked this conversation as resolved.
- 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 Maven project file to: `{scripts_dir}/pom.xml`
43 changes: 43 additions & 0 deletions tests/test_base_engineer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for base_engineer.py - BaseEngineer abstract class."""

import shlex
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
Expand Down Expand Up @@ -197,6 +198,10 @@ def test_get_output_extension_go(self, tmp_path):
"""Go extension."""
eng = self._make_engineer(tmp_path, output_language="go")
assert eng._get_output_extension() == ".go"
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_unknown(self, tmp_path):
"""Unknown language defaults to .py."""
Expand Down Expand Up @@ -232,6 +237,38 @@ def test_get_run_command_go(self, tmp_path):
"""Run command for Go."""
eng = self._make_engineer(tmp_path, output_language="go")
assert eng._get_run_command() == "go run api_client.go"
def test_get_run_command_java(self, tmp_path):
"""Run command for Java points -f at this run's own (resolved,
shell-quoted) pom.xml, not a bare relative path — the agent's cwd is
scripts_dir.parent.parent (see analyze_and_generate), and unlike
python/node/npx, Maven hard-fails with no upward search if invoked
from a directory with no pom.xml."""
eng = self._make_engineer(tmp_path, output_language="java")
expected_pom = shlex.quote(str(eng.scripts_dir.resolve() / "pom.xml"))
assert eng._get_run_command() == f"mvn -q -f {expected_pom} compile exec:exec"

def test_get_run_command_java_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="java")
eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir")
tokens = shlex.split(eng._get_run_command())
assert tokens[:2] == ["mvn", "-q"]
assert tokens[3] == str(eng.scripts_dir.resolve() / "pom.xml")

def test_get_run_command_java_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="java")
eng.scripts_dir = Path("relative_output/scripts/run123")
tokens = shlex.split(eng._get_run_command())
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_unknown(self, tmp_path):
"""Unknown language defaults to Python command."""
Expand Down Expand Up @@ -285,6 +322,12 @@ def test_go_prompt(self, tmp_path):
system_prompt, user_message = eng._build_prompts()
assert "Go program" in system_prompt
assert "net/http" in system_prompt
def test_java_prompt(self, tmp_path):
"""Java prompt includes Java-specific instructions."""
eng = self._make_engineer(tmp_path, output_language="java")
system_prompt, user_message = eng._build_prompts()
assert "Java program" in system_prompt
assert "HttpClient" 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 @@ -81,6 +81,16 @@ def test_go_partial(self):
assert "Go program" in text
assert "net/http" in text
assert "/tmp/scripts/api_client.go" in text
def test_java_partial(self):
text = load_language_partial(
"java",
scripts_dir="/tmp/scripts",
client_filename="api_client.java",
run_command="mvn -q compile exec:exec",
)
assert "Java program" in text
assert "HttpClient" in text
assert "/tmp/scripts/api_client.java" in text


class TestEngineerTemplates:
Expand Down