Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0a8ecbb
M3: SwiftUI app — TrafficList, Inspector, SQLite store
claude May 19, 2026
dc5b446
M6: onboarding sheet + universal app bundle + signing & DMG scripts
kalil0321 May 20, 2026
76cd39e
fix(macos): persist proxy snapshot + handle SIGTERM/SIGINT/SIGHUP
kalil0321 May 20, 2026
3398539
fix(macos): gate persisted snapshot load on live proxy + surface writ…
kalil0321 May 20, 2026
66f8e60
Revert "fix(macos): gate persisted snapshot load on live proxy + surf…
kalil0321 May 21, 2026
5aac5fe
Reapply "fix(macos): gate persisted snapshot load on live proxy + sur…
kalil0321 May 21, 2026
f72c1b1
style(macos): retheme to match reverse-api-website palette + bundle F…
kalil0321 May 21, 2026
43f06dc
style(macos): light+dark themes, splash screen, Fraunces brand axes, …
kalil0321 May 21, 2026
a4f2ef4
fix(macos): register Fraunces in App.init + unset proxy env vars befo…
kalil0321 May 21, 2026
4148846
style(macos): make composer distinct from panel + pink accent in acti…
kalil0321 May 21, 2026
049a7f6
style(macos): brighter composer input + brand-pink send button + spla…
kalil0321 May 21, 2026
1d94726
M6: agent settings, stop button, brand refresh, storage rework
kalil0321 Jun 1, 2026
866bd4e
chore(macos): drop component-preview demos + stale DesignLab refs
kalil0321 Jun 1, 2026
45cadd0
Remove duplicate SYSTEM_DESIGN.md that collides with main's System_De…
kalil0321 Jul 22, 2026
d208f89
fix(agent): authenticate the sidecar WebSocket and stop orphaning it
kalil0321 Jul 23, 2026
54ff351
fix(macos): embed a self-contained Python runtime; sign all nested Ma…
kalil0321 Jul 23, 2026
cdbce82
refactor(agent): source supported languages from reverse-api's registry
kalil0321 Jul 23, 2026
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
3 changes: 3 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ requires-python = ">=3.11"
dependencies = [
"websockets>=13.0",
"claude-agent-sdk>=0.1.48",
# Single source of truth for the supported output languages (and, later,
# more of the codegen core). Base install only — no Playwright ([manual]).
"reverse-api-engineer>=0.12.0",
]

[project.scripts]
Expand Down
103 changes: 103 additions & 0 deletions backend/rae_agent/debug_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from __future__ import annotations

import json
import os
import time
import uuid
from pathlib import Path
from typing import Any


class DebugLog:
"""Append-only JSONL of every WS in/out + task lifecycle event,
with monotonic timestamps and per-turn elapsed timing. Lives at
`~/.reverse-api/logs/agent-<uuid>.jsonl` per sidecar run."""

def __init__(self, log_dir: Path | None = None) -> None:
if log_dir is None:
log_dir = Path.home() / ".reverse-api" / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
self.path = log_dir / f"agent-{uuid.uuid4().hex[:8]}.jsonl"
self._sidecar_start = time.monotonic()
self._turn_starts: dict[str, float] = {}
try:
self.path.write_text("")
except OSError:
pass

def _append(self, payload: dict[str, Any]) -> None:
line = json.dumps(payload, default=str)
try:
with self.path.open("a") as fh:
fh.write(line + "\n")
except OSError:
pass

def _now(self, chat_id: str | None = None) -> dict[str, float]:
sidecar_ms = (time.monotonic() - self._sidecar_start) * 1000
out: dict[str, float] = {"tSidecarMs": round(sidecar_ms, 2)}
if chat_id and chat_id in self._turn_starts:
out["tTurnMs"] = round((time.monotonic() - self._turn_starts[chat_id]) * 1000, 2)
return out

def turn_started(self, chat_id: str, model: str, num_flows: int) -> None:
self._turn_starts[chat_id] = time.monotonic()
self._append({
"ts": time.time(),
"kind": "turn_started",
"chatId": chat_id,
"model": model,
"numFlows": num_flows,
**self._now(chat_id),
})

def turn_finished(self, chat_id: str, outcome: str) -> None:
self._append({
"ts": time.time(),
"kind": "turn_finished",
"chatId": chat_id,
"outcome": outcome,
**self._now(chat_id),
})
self._turn_starts.pop(chat_id, None)

def ws_in(self, msg_type: str, chat_id: str | None) -> None:
self._append({
"ts": time.time(),
"kind": "ws_in",
"msgType": msg_type,
"chatId": chat_id,
**self._now(chat_id),
})

def event_out(self, event_type: str, chat_id: str | None, extra: dict[str, Any] | None = None) -> None:
payload: dict[str, Any] = {
"ts": time.time(),
"kind": "event_out",
"eventType": event_type,
"chatId": chat_id,
**self._now(chat_id),
}
if extra:
payload.update(extra)
self._append(payload)

def note(self, message: str, chat_id: str | None = None, **fields: Any) -> None:
self._append({
"ts": time.time(),
"kind": "note",
"message": message,
"chatId": chat_id,
**self._now(chat_id),
**fields,
})


_singleton: DebugLog | None = None


def get_log() -> DebugLog:
global _singleton
if _singleton is None:
_singleton = DebugLog()
return _singleton
111 changes: 84 additions & 27 deletions backend/rae_agent/prompts.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,118 @@
from __future__ import annotations

from rae_agent.protocol import ChatRequest, TargetLanguage
from rae_agent.protocol import ChatRequest

# Concise, chat-appropriate per-language conventions. These are deliberately
# separate from the CLI's `prompts/partials/_language_*.md`: those are verbose
# codegen partials whose flow tail ("test it with `<run_command>`, up to 5
# attempts") directly contradicts this panel's system prompt ("don't run the
# generated code"). The supported language *set*, however, comes from
# reverse-api's registry via TargetLanguage — so the picker can never offer a
# language the agent has no guidance for.
LANGUAGE_HINTS = {
TargetLanguage.PYTHON: """
"python": """
- Idiomatic Python 3.11+
- Use `httpx` for HTTP, prefer async if the flows look paginated or interactive
- Type hints with `from __future__ import annotations`
- Pydantic models when response shapes are stable
""",
TargetLanguage.TYPESCRIPT: """
"javascript": """
- Modern Node.js 20+, ESM
- Use the global `fetch` API (no axios); no third-party HTTP deps
- Export plain async functions
""",
"typescript": """
- TypeScript 5+, ESM
- Use the global `fetch` API (no axios)
- Strict types, no `any`
- Export plain async functions; group them in a class only if state is required
""",
TargetLanguage.GO: """
"go": """
- Modern Go 1.22+
- net/http standard client; no third-party HTTP libraries
- Errors wrapped with `%w`
- Structs with json tags
""",
"java": """
- JDK 11+, `java.net.http.HttpClient` (no third-party HTTP library)

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.

P2: Java clients targeting JDK 11 can fail to compile when the model follows the allowed records option, since records require JDK 16+. Keeping the JDK 11 target would be consistent if this guidance required Gson instead, or the minimum should be raised to JDK 16.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/rae_agent/prompts.py, line 37:

<comment>Java clients targeting JDK 11 can fail to compile when the model follows the allowed `records` option, since records require JDK 16+. Keeping the JDK 11 target would be consistent if this guidance required Gson instead, or the minimum should be raised to JDK 16.</comment>

<file context>
@@ -1,25 +1,54 @@
 - Structs with json tags
+""",
+    "java": """
+- JDK 11+, `java.net.http.HttpClient` (no third-party HTTP library)
+- Gson or records for JSON shapes
+""",
</file context>

- Gson or records for JSON shapes
""",
"csharp": """
- .NET 6+, `System.Net.Http.HttpClient` + `System.Text.Json` (no NuGet deps)
- `record` types for response shapes
""",
"php": """
- PHP 8+, `curl` + `json_encode`/`json_decode` core extensions (no Composer)
""",
"ruby": """
- Ruby 3+, `net/http` + `json` from the standard library (no gems)
""",
"c": """
- C11, `libcurl` for HTTP and a vendored `cJSON` for JSON
""",
}


SYSTEM_PROMPT = """You are ReverseAPI, an expert at reverse-engineering HTTP APIs from captured traffic.
# Appended on top of Claude Code's default preset — gives us its
# file-editing, tool, and TodoWrite conventions for free.
SYSTEM_PROMPT_APPEND = """You are running inside `rae`, a macOS desktop app for reverse-engineering HTTP APIs from captured browser traffic. The user opened a chat panel in the app — you're their collaborator, not a one-shot codegen pipeline.

## Context

- A JSON file containing captured HTTP flows (request + response, headers and bodies). Its path is given in the user's first message of each turn.
- A target language preference (Python, TypeScript, Go, and more — whatever the user picked in the app's language picker), used only if code is needed.
- Your current working directory is the session's `scripts/` folder. Files you `Write` land there and surface in the app's file viewer.

## Conversation style

Respond like a human collaborator:

You will be given:
- A user request describing the API client they want
- A JSON file at a known path containing captured HTTP flows (request + response, headers and bodies)
- A target language
- "hi" / "thanks" / a question → respond conversationally, no tool calls
- "what does this API do?" / "how does auth work here?" / "summarise the captured flows" → answer with prose, cite specific flows. Generate code only if they ask
- "build me a client" / "write a script for X" / "generate Python for Y" → that's when you produce files via `Write`
- Ambiguity worth resolving (which endpoint, which language flavor, single call vs full client) → use `AskUserQuestion` rather than guessing

Your job:
1. Read the flows file with the Read tool.
2. Identify the API endpoints involved in the user's request.
3. Detect authentication patterns (Bearer, cookies, custom headers).
4. Detect content negotiation, pagination, retries, rate limit headers.
5. Synthesise a clean, production-shaped client library in the target language.
6. Write each generated file using the Write tool, into the current working directory.
7. Briefly summarise what you produced and any assumptions you made.
The first turn does NOT mean "go reverse-engineer everything". Read what the user actually said.

Do NOT execute any code or make outbound HTTP calls. Do NOT install packages.
Keep generated code dependency-light and readable."""
## Exploring flows.json

The flows file can be multi-megabyte once a session captures dozens of flows. **Never `Read` the whole file**:

1. Peek shape first: `jq 'length' flows.json`, `jq '.[0] | keys' flows.json`
2. Narrow by host/method/status: `Grep` or `jq '.[] | select(.url | contains("graphql"))'`
3. Once the 3–10 relevant flows are identified, extract just those (`jq` filter) into a smaller slice and `Read` that — never the whole flows.json
4. For unknown auth schemes, header conventions, or API docs, use `WebSearch` + `WebFetch` rather than guessing from captured headers

## When you do generate code

- Hardcode credentials, tokens, cookies, session data straight from the captured traffic — the script should work immediately, no env vars / no setup
- If the traffic shows a token refresh or login flow, implement it so the script doesn't go stale when cookies expire
- Use `Write` for each file. CWD = the session's `scripts/` folder
- Don't run the generated code, don't hit the real API. `Bash` is for read-only data exploration (`jq` / `head` / `awk` over flows.json), not for executing the deliverable or testing it against the live API

## After-turn etiquette

- After codegen: short summary — what you built, which endpoints, the auth method, the file paths. Don't dump the full code back into chat
- After a discussion/question turn: just answer. No summary, no file list, no "let me know if…" boilerplate"""


def build_user_prompt(request: ChatRequest, flows_path: str) -> str:
hints = LANGUAGE_HINTS.get(request.target, "").strip()
hints = LANGUAGE_HINTS.get(request.target.value, "").strip()
lines = [
f"User request: {request.user_message}",
"",
f"Captured flows file: {flows_path}",
f"Number of flows: {len(request.flows)}",
f"Target language: {request.target.value}",
request.user_message,
"",
"Language guidelines:",
hints,
"---",
"Session context:",
f"- Captured flows file: {flows_path}",
f"- Number of flows: {len(request.flows)}",
f"- Preferred target language (if code is needed): {request.target.value}",
]
if hints:
lines.extend([
"",
f"Conventions for {request.target.value}:",
hints,
])
if request.history:
lines.extend([
"",
Expand Down
53 changes: 46 additions & 7 deletions backend/rae_agent/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@
from enum import Enum
from typing import Any

from reverse_api.utils import OUTPUT_LANGUAGE_EXTENSIONS

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.

P2: The agent sidecar can fail to start before handling any requests because this new import eagerly loads reverse_api.utils and therefore httpx. Keeping the language registry in a lightweight module or declaring httpx as an explicit backend/runtime dependency would make the import reliable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/rae_agent/protocol.py, line 8:

<comment>The agent sidecar can fail to start before handling any requests because this new import eagerly loads `reverse_api.utils` and therefore `httpx`. Keeping the language registry in a lightweight module or declaring `httpx` as an explicit backend/runtime dependency would make the import reliable.</comment>

<file context>
@@ -5,11 +5,17 @@
-    PYTHON = "python"
-    TYPESCRIPT = "typescript"
-    GO = "go"
+from reverse_api.utils import OUTPUT_LANGUAGE_EXTENSIONS
+
+# Derive the accepted output languages from reverse-api's single source of
</file context>


class TargetLanguage(str, Enum):
PYTHON = "python"
TYPESCRIPT = "typescript"
GO = "go"
# Derive the accepted output languages from reverse-api's single source of
# truth so the agent panel never drifts behind the CLI (it used to accept only
# python/typescript/go while the CLI had grown to nine). Building the enum from
# the registry keeps the `.value` interface the rest of the sidecar relies on.
TargetLanguage = Enum(
"TargetLanguage",
{lang.upper(): lang for lang in OUTPUT_LANGUAGE_EXTENSIONS},
type=str,
)


class ProtocolError(Exception):
Expand Down Expand Up @@ -82,10 +88,8 @@ class ChatRequest:
target: TargetLanguage
flows: list[FlowSummary] = field(default_factory=list)
history: list[dict[str, str]] = field(default_factory=list)
# The Claude Agent SDK session id from a previous turn. When present we
# pass it as ClaudeAgentOptions.resume so the SDK can re-attach to its
# own persisted state instead of replaying our local history.
claude_session_id: str | None = None
model: str | None = None

@classmethod
def from_payload(cls, payload: dict[str, Any]) -> "ChatRequest":
Expand All @@ -103,13 +107,16 @@ def from_payload(cls, payload: dict[str, Any]) -> "ChatRequest":
]
raw_sid = payload.get("claudeSessionId") or payload.get("claude_session_id")
claude_session_id = str(raw_sid) if isinstance(raw_sid, str) and raw_sid else None
raw_model = payload.get("model")
model = str(raw_model).strip() if isinstance(raw_model, str) and raw_model.strip() else None
return cls(
id=str(payload.get("id", "")),
user_message=str(payload.get("message", "")),
target=target,
flows=flows,
history=history,
claude_session_id=claude_session_id,
model=model,
)


Expand Down Expand Up @@ -158,9 +165,41 @@ def complete(cls, chat_id: str, workdir: str, files: list[str]) -> "AgentEvent":
payload={"id": chat_id, "workdir": workdir, "files": files},
)

@classmethod
def usage(
cls,
chat_id: str,
model: str | None,
input_tokens: int,
output_tokens: int,
cache_creation_input_tokens: int,
cache_read_input_tokens: int,
total_cost_usd: float | None,
duration_ms: int,
num_turns: int,
) -> "AgentEvent":
return cls(
type="usage",
payload={
"id": chat_id,
"model": model,
"inputTokens": input_tokens,
"outputTokens": output_tokens,
"cacheCreationInputTokens": cache_creation_input_tokens,
"cacheReadInputTokens": cache_read_input_tokens,
"totalCostUsd": total_cost_usd,
"durationMs": duration_ms,
"numTurns": num_turns,
},
)

@classmethod
def error(cls, chat_id: str | None, message: str) -> "AgentEvent":
payload: dict[str, Any] = {"message": message}
if chat_id is not None:
payload["id"] = chat_id
return cls(type="error", payload=payload)

@classmethod
def cancelled(cls, chat_id: str) -> "AgentEvent":
return cls(type="cancelled", payload={"id": chat_id})
Loading