reverse-api-engineer 0.4.2__tar.gz → 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.claude/settings.local.json +3 -1
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/CHANGELOG.md +5 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/PKG-INFO +1 -1
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/pyproject.toml +1 -1
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/auto_engineer.py +109 -96
- reverse_api_engineer-0.4.3/src/reverse_api/engineer.py +255 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/uv.lock +1 -1
- reverse_api_engineer-0.4.2/src/reverse_api/engineer.py +0 -253
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.claude-plugin/marketplace.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.gitignore +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.python-version +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/CLAUDE.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/CONTRIBUTING.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/INTERVIEW.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/LICENSE +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/PROMPT_STASH.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/RELEASING.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/assets/reverse-api-banner.svg +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/assets/reverse-api-engineer.gif +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/assets/reverse-api-logo.svg +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/.claude/settings.local.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/components.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/package-lock.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/package.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/postcss.config.js +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/_locales/en/messages.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-128.png +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-16.png +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-32.png +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-48.png +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/manifest.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/background/service-worker.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/agent-action.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/chat-input.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/icons.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/markdown-renderer.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/mode-selector.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/plan.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/session-selector.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/terminal.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/ui/code-block.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/content/codegen-recorder.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/index.css +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/capture.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/native-host.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/storage.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/types.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/sidepanel/index.html +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/sidepanel/main.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/sidepanel/side-panel.tsx +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/vite-env.d.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/tailwind.config.js +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/tsconfig.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/vite.config.ts +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/INDEX.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/QUICKSTART.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/SUMMARY.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/api_client.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/extract_job_fields.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/main.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/quick_example.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/requirements.txt +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/API_SUMMARY.txt +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/QUICKSTART.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/api_client.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/example_usage.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/requirements.txt +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/autoscout24/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/autoscout24/SUMMARY.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/autoscout24/api_client.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ikea/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ikea/api_client.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/mintlify/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/mintlify/api_client.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/API_ANALYSIS_SUMMARY.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/api_client.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/example_fetch_all_jobs.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/quick_start.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/requirements.txt +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/OPENCODE_API_SUMMARY.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/claude-agent-sdk/QUICKSTART.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/claude-agent-sdk/TODO_LIST.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/claude-agent-sdk/TOOLS.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/opencode-api.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/.claude-plugin/plugin.json +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/CHANGELOG.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/LICENSE +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/README.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/agents/api-reverse-engineer.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/agent.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/capture.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/engineer.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/manual.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/CHANGELOG.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/LICENSE +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/SKILL.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/AUTH_PATTERNS.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/HAR_ANALYSIS.md +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_analyze.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_filter.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_utils.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_validate.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/templates/api_client.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/scripts/clean_build.sh +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/__init__.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/action_recorder.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/base_engineer.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/browser.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/cli.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/collector.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/collector_ui.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/config.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/copilot_engineer.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/messages.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/native_host.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/opencode_engineer.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/opencode_ui.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/playwright_codegen.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/pricing.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/session.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/sync.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/tui.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/utils.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/__init__.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/conftest.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_action_recorder.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_auto_engineer.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_base_engineer.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_collector.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_collector_ui.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_config.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_engineer.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_init.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_messages.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_native_host.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_opencode_engineer.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_opencode_ui.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_pricing.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_session.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_sync.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_tui.py +0 -0
- {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_utils.py +0 -0
|
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.3] - 2026-03-12
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **AskUserQuestion interactive prompt**: Fixed interactive questionary UI not rendering in engineer and agent modes. Switched from `ClaudeSDKClient` to `query()` function, as `ClaudeSDKClient` silently auto-approves `AskUserQuestion` without triggering the `can_use_tool` callback
|
|
12
|
+
|
|
8
13
|
## [0.4.2] - 2026-03-12
|
|
9
14
|
|
|
10
15
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reverse-api-engineer
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: A tool to capture browser traffic for API reverse engineering
|
|
5
5
|
Project-URL: Homepage, https://github.com/kalil0321/reverse-api-engineer
|
|
6
6
|
Project-URL: Repository, https://github.com/kalil0321/reverse-api-engineer
|
|
@@ -11,11 +11,14 @@ import httpx
|
|
|
11
11
|
from claude_agent_sdk import (
|
|
12
12
|
AssistantMessage,
|
|
13
13
|
ClaudeAgentOptions,
|
|
14
|
-
|
|
14
|
+
HookMatcher,
|
|
15
|
+
PermissionResultAllow,
|
|
15
16
|
ResultMessage,
|
|
16
17
|
TextBlock,
|
|
18
|
+
ToolPermissionContext,
|
|
17
19
|
ToolResultBlock,
|
|
18
20
|
ToolUseBlock,
|
|
21
|
+
query,
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
from .engineer import ClaudeEngineer
|
|
@@ -226,6 +229,19 @@ Your final response should confirm the files were created and provide a brief su
|
|
|
226
229
|
- Any limitations or caveats
|
|
227
230
|
"""
|
|
228
231
|
|
|
232
|
+
async def _handle_tool_permission(
|
|
233
|
+
self, tool_name: str, input_data: dict[str, Any], context: ToolPermissionContext
|
|
234
|
+
) -> PermissionResultAllow:
|
|
235
|
+
"""Handle tool permission requests, with interactive UI for AskUserQuestion."""
|
|
236
|
+
if tool_name == "AskUserQuestion":
|
|
237
|
+
questions = input_data.get("questions", [])
|
|
238
|
+
answers = await self._ask_user_interactive(questions)
|
|
239
|
+
return PermissionResultAllow(
|
|
240
|
+
updated_input={"questions": questions, "answers": answers},
|
|
241
|
+
)
|
|
242
|
+
# Auto-approve all other tools
|
|
243
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
244
|
+
|
|
229
245
|
async def analyze_and_generate(self) -> dict[str, Any] | None:
|
|
230
246
|
"""Run auto mode with MCP browser integration."""
|
|
231
247
|
self.ui.header(self.run_id, self.prompt, self.model)
|
|
@@ -243,119 +259,116 @@ Your final response should confirm the files were created and provide a brief su
|
|
|
243
259
|
],
|
|
244
260
|
}
|
|
245
261
|
|
|
262
|
+
# Required workaround: dummy hook keeps the stream open for can_use_tool
|
|
263
|
+
# See: https://platform.claude.com/docs/en/agent-sdk/user-input
|
|
264
|
+
async def _dummy_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> dict[str, Any]:
|
|
265
|
+
return {"continue_": True}
|
|
266
|
+
|
|
267
|
+
prompt_text = self._build_auto_prompt()
|
|
268
|
+
|
|
269
|
+
async def _prompt_stream() -> Any:
|
|
270
|
+
yield {
|
|
271
|
+
"type": "user",
|
|
272
|
+
"message": {"role": "user", "content": prompt_text},
|
|
273
|
+
}
|
|
274
|
+
|
|
246
275
|
options = ClaudeAgentOptions(
|
|
247
276
|
mcp_servers={"playwright": mcp_config},
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"Read",
|
|
251
|
-
"Write",
|
|
252
|
-
"Bash",
|
|
253
|
-
"Glob",
|
|
254
|
-
"Grep",
|
|
255
|
-
"WebSearch",
|
|
256
|
-
"WebFetch",
|
|
257
|
-
],
|
|
277
|
+
can_use_tool=self._handle_tool_permission,
|
|
278
|
+
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[_dummy_hook])]},
|
|
258
279
|
cwd=str(self.scripts_dir.parent.parent), # Project root
|
|
259
280
|
model=self.model,
|
|
260
281
|
)
|
|
261
282
|
|
|
262
283
|
try:
|
|
263
|
-
async
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
output =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
self.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
self.ui.console.print(f" [dim]
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
result: dict[str, Any] = {
|
|
340
|
-
"script_path": script_path,
|
|
341
|
-
"usage": self.usage_metadata,
|
|
342
|
-
}
|
|
343
|
-
self.message_store.save_result(result)
|
|
344
|
-
return result
|
|
284
|
+
async for message in query(prompt=_prompt_stream(), options=options):
|
|
285
|
+
# Check for usage metadata
|
|
286
|
+
if hasattr(message, "usage") and isinstance(message.usage, dict):
|
|
287
|
+
self.usage_metadata.update(message.usage)
|
|
288
|
+
|
|
289
|
+
if isinstance(message, AssistantMessage):
|
|
290
|
+
last_tool_name = None
|
|
291
|
+
for block in message.content:
|
|
292
|
+
if isinstance(block, ToolUseBlock):
|
|
293
|
+
last_tool_name = block.name
|
|
294
|
+
self.ui.tool_start(block.name, block.input)
|
|
295
|
+
self.message_store.save_tool_start(block.name, block.input)
|
|
296
|
+
elif isinstance(block, ToolResultBlock):
|
|
297
|
+
is_error = block.is_error if block.is_error else False
|
|
298
|
+
|
|
299
|
+
# Extract output from ToolResultBlock
|
|
300
|
+
output = None
|
|
301
|
+
if hasattr(block, "content"):
|
|
302
|
+
output = block.content
|
|
303
|
+
elif hasattr(block, "result"):
|
|
304
|
+
output = block.result
|
|
305
|
+
elif hasattr(block, "output"):
|
|
306
|
+
output = block.output
|
|
307
|
+
|
|
308
|
+
tool_name = last_tool_name or "Tool"
|
|
309
|
+
self.ui.tool_result(tool_name, is_error, output)
|
|
310
|
+
self.message_store.save_tool_result(tool_name, is_error, str(output) if output else None)
|
|
311
|
+
elif isinstance(block, TextBlock):
|
|
312
|
+
self.ui.thinking(block.text)
|
|
313
|
+
self.message_store.save_thinking(block.text)
|
|
314
|
+
|
|
315
|
+
elif isinstance(message, ResultMessage):
|
|
316
|
+
if message.is_error:
|
|
317
|
+
self.ui.error(message.result or "Unknown error")
|
|
318
|
+
self.message_store.save_error(message.result or "Unknown error")
|
|
319
|
+
return None
|
|
320
|
+
else:
|
|
321
|
+
script_path = str(self.scripts_dir / self._get_client_filename())
|
|
322
|
+
local_path = str(self.local_scripts_dir / self._get_client_filename()) if self.local_scripts_dir else None
|
|
323
|
+
self.ui.success(script_path, local_path)
|
|
324
|
+
|
|
325
|
+
# Calculate estimated cost if we have usage data
|
|
326
|
+
if self.usage_metadata:
|
|
327
|
+
input_tokens = self.usage_metadata.get("input_tokens", 0)
|
|
328
|
+
output_tokens = self.usage_metadata.get("output_tokens", 0)
|
|
329
|
+
cache_creation_tokens = self.usage_metadata.get("cache_creation_input_tokens", 0)
|
|
330
|
+
cache_read_tokens = self.usage_metadata.get("cache_read_input_tokens", 0)
|
|
331
|
+
|
|
332
|
+
from .pricing import calculate_cost
|
|
333
|
+
|
|
334
|
+
cost = calculate_cost(
|
|
335
|
+
model_id=self.model,
|
|
336
|
+
input_tokens=input_tokens,
|
|
337
|
+
output_tokens=output_tokens,
|
|
338
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
339
|
+
cache_read_tokens=cache_read_tokens,
|
|
340
|
+
)
|
|
341
|
+
self.usage_metadata["estimated_cost_usd"] = cost
|
|
342
|
+
|
|
343
|
+
self.ui.console.print(" [dim]Usage:[/dim]")
|
|
344
|
+
if input_tokens > 0:
|
|
345
|
+
self.ui.console.print(f" [dim] input: {input_tokens:,} tokens[/dim]")
|
|
346
|
+
if cache_creation_tokens > 0:
|
|
347
|
+
self.ui.console.print(f" [dim] cache creation: {cache_creation_tokens:,} tokens[/dim]")
|
|
348
|
+
if cache_read_tokens > 0:
|
|
349
|
+
self.ui.console.print(f" [dim] cache read: {cache_read_tokens:,} tokens[/dim]")
|
|
350
|
+
if output_tokens > 0:
|
|
351
|
+
self.ui.console.print(f" [dim] output: {output_tokens:,} tokens[/dim]")
|
|
352
|
+
self.ui.console.print(f" [dim] total cost: ${cost:.4f}[/dim]")
|
|
353
|
+
|
|
354
|
+
result: dict[str, Any] = {
|
|
355
|
+
"script_path": script_path,
|
|
356
|
+
"usage": self.usage_metadata,
|
|
357
|
+
}
|
|
358
|
+
self.message_store.save_result(result)
|
|
359
|
+
return result
|
|
345
360
|
|
|
346
361
|
except Exception as e:
|
|
347
362
|
error_msg = str(e)
|
|
348
363
|
self.ui.error(error_msg)
|
|
349
364
|
self.message_store.save_error(error_msg)
|
|
350
365
|
|
|
351
|
-
# Handle screenshot buffer size errors specifically
|
|
352
366
|
if "buffer size" in error_msg.lower() or "1048576" in error_msg or "exceeded maximum buffer" in error_msg.lower():
|
|
353
|
-
self.ui.console.print("\n[yellow]
|
|
367
|
+
self.ui.console.print("\n[yellow]Screenshot too large (exceeds 1MB limit)[/yellow]")
|
|
354
368
|
self.ui.console.print("[dim]Tip: The AI should take element-specific screenshots instead of full-page screenshots.[/dim]")
|
|
355
369
|
self.ui.console.print(
|
|
356
370
|
"[dim]Consider using browser_snapshot() for accessibility tree information when screenshots aren't needed.[/dim]"
|
|
357
371
|
)
|
|
358
|
-
# Provide helpful error messages
|
|
359
372
|
elif "MCP server" in error_msg or "npx" in error_msg:
|
|
360
373
|
self.ui.console.print("\n[dim]Make sure rae-playwright-mcp is installed: npm install -g rae-playwright-mcp[/dim]")
|
|
361
374
|
else:
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Reverse engineering module with SDK dispatch."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from claude_agent_sdk import (
|
|
9
|
+
AssistantMessage,
|
|
10
|
+
ClaudeAgentOptions,
|
|
11
|
+
HookMatcher,
|
|
12
|
+
PermissionResultAllow,
|
|
13
|
+
ResultMessage,
|
|
14
|
+
TextBlock,
|
|
15
|
+
ToolPermissionContext,
|
|
16
|
+
ToolResultBlock,
|
|
17
|
+
ToolUseBlock,
|
|
18
|
+
query,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .base_engineer import BaseEngineer
|
|
22
|
+
|
|
23
|
+
# Suppress claude_agent_sdk logs
|
|
24
|
+
logging.getLogger("claude_agent_sdk").setLevel(logging.WARNING)
|
|
25
|
+
logging.getLogger("claude_agent_sdk._internal.transport.subprocess_cli").setLevel(logging.WARNING)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClaudeEngineer(BaseEngineer):
|
|
29
|
+
"""Uses Claude Agent SDK to analyze HAR files and generate Python API scripts."""
|
|
30
|
+
|
|
31
|
+
async def _handle_tool_permission(
|
|
32
|
+
self, tool_name: str, input_data: dict[str, Any], context: ToolPermissionContext
|
|
33
|
+
) -> PermissionResultAllow:
|
|
34
|
+
"""Handle tool permission requests, with interactive UI for AskUserQuestion."""
|
|
35
|
+
if tool_name == "AskUserQuestion":
|
|
36
|
+
questions = input_data.get("questions", [])
|
|
37
|
+
answers = await self._ask_user_interactive(questions)
|
|
38
|
+
return PermissionResultAllow(
|
|
39
|
+
updated_input={"questions": questions, "answers": answers},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Auto-approve all other tools
|
|
43
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
44
|
+
|
|
45
|
+
async def analyze_and_generate(self) -> dict[str, Any] | None:
|
|
46
|
+
"""Run the reverse engineering analysis with Claude."""
|
|
47
|
+
self.ui.header(self.run_id, self.prompt, self.model, self.sdk)
|
|
48
|
+
self.ui.start_analysis()
|
|
49
|
+
self.message_store.save_prompt(self._build_analysis_prompt())
|
|
50
|
+
|
|
51
|
+
# Required workaround: dummy hook keeps the stream open for can_use_tool
|
|
52
|
+
# See: https://platform.claude.com/docs/en/agent-sdk/user-input
|
|
53
|
+
async def _dummy_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> dict[str, Any]:
|
|
54
|
+
return {"continue_": True}
|
|
55
|
+
|
|
56
|
+
prompt_text = self._build_analysis_prompt()
|
|
57
|
+
|
|
58
|
+
async def _prompt_stream() -> Any:
|
|
59
|
+
yield {
|
|
60
|
+
"type": "user",
|
|
61
|
+
"message": {"role": "user", "content": prompt_text},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
options = ClaudeAgentOptions(
|
|
65
|
+
can_use_tool=self._handle_tool_permission,
|
|
66
|
+
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[_dummy_hook])]},
|
|
67
|
+
cwd=str(self.scripts_dir.parent.parent), # Project root
|
|
68
|
+
model=self.model,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
async for message in query(prompt=_prompt_stream(), options=options):
|
|
73
|
+
# Check for usage metadata in message if applicable
|
|
74
|
+
if hasattr(message, "usage") and isinstance(message.usage, dict):
|
|
75
|
+
self.usage_metadata.update(message.usage)
|
|
76
|
+
|
|
77
|
+
if isinstance(message, AssistantMessage):
|
|
78
|
+
last_tool_name = None
|
|
79
|
+
for block in message.content:
|
|
80
|
+
if isinstance(block, ToolUseBlock):
|
|
81
|
+
last_tool_name = block.name
|
|
82
|
+
self.ui.tool_start(block.name, block.input)
|
|
83
|
+
self.message_store.save_tool_start(block.name, block.input)
|
|
84
|
+
elif isinstance(block, ToolResultBlock):
|
|
85
|
+
is_error = block.is_error if block.is_error else False
|
|
86
|
+
|
|
87
|
+
# Extract output from ToolResultBlock
|
|
88
|
+
output = None
|
|
89
|
+
if hasattr(block, "content"):
|
|
90
|
+
output = block.content
|
|
91
|
+
elif hasattr(block, "result"):
|
|
92
|
+
output = block.result
|
|
93
|
+
elif hasattr(block, "output"):
|
|
94
|
+
output = block.output
|
|
95
|
+
|
|
96
|
+
tool_name = last_tool_name or "Tool"
|
|
97
|
+
self.ui.tool_result(tool_name, is_error, output)
|
|
98
|
+
self.message_store.save_tool_result(tool_name, is_error, str(output) if output else None)
|
|
99
|
+
elif isinstance(block, TextBlock):
|
|
100
|
+
self.ui.thinking(block.text)
|
|
101
|
+
self.message_store.save_thinking(block.text)
|
|
102
|
+
|
|
103
|
+
elif isinstance(message, ResultMessage):
|
|
104
|
+
if message.is_error:
|
|
105
|
+
self.ui.error(message.result or "Unknown error")
|
|
106
|
+
self.message_store.save_error(message.result or "Unknown error")
|
|
107
|
+
return None
|
|
108
|
+
else:
|
|
109
|
+
script_path = str(self.scripts_dir / self._get_client_filename())
|
|
110
|
+
local_path = str(self.local_scripts_dir / self._get_client_filename()) if self.local_scripts_dir else None
|
|
111
|
+
self.ui.success(script_path, local_path)
|
|
112
|
+
|
|
113
|
+
# Calculate estimated cost if we have usage data
|
|
114
|
+
if self.usage_metadata:
|
|
115
|
+
input_tokens = self.usage_metadata.get("input_tokens", 0)
|
|
116
|
+
output_tokens = self.usage_metadata.get("output_tokens", 0)
|
|
117
|
+
cache_creation_tokens = self.usage_metadata.get("cache_creation_input_tokens", 0)
|
|
118
|
+
cache_read_tokens = self.usage_metadata.get("cache_read_input_tokens", 0)
|
|
119
|
+
|
|
120
|
+
# Calculate cost using shared pricing module
|
|
121
|
+
from .pricing import calculate_cost
|
|
122
|
+
|
|
123
|
+
cost = calculate_cost(
|
|
124
|
+
model_id=self.model,
|
|
125
|
+
input_tokens=input_tokens,
|
|
126
|
+
output_tokens=output_tokens,
|
|
127
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
128
|
+
cache_read_tokens=cache_read_tokens,
|
|
129
|
+
)
|
|
130
|
+
self.usage_metadata["estimated_cost_usd"] = cost
|
|
131
|
+
|
|
132
|
+
# Display usage breakdown
|
|
133
|
+
self.ui.console.print(" [dim]Usage:[/dim]")
|
|
134
|
+
if input_tokens > 0:
|
|
135
|
+
self.ui.console.print(f" [dim] input: {input_tokens:,} tokens[/dim]")
|
|
136
|
+
if cache_creation_tokens > 0:
|
|
137
|
+
self.ui.console.print(f" [dim] cache creation: {cache_creation_tokens:,} tokens[/dim]")
|
|
138
|
+
if cache_read_tokens > 0:
|
|
139
|
+
self.ui.console.print(f" [dim] cache read: {cache_read_tokens:,} tokens[/dim]")
|
|
140
|
+
if output_tokens > 0:
|
|
141
|
+
self.ui.console.print(f" [dim] output: {output_tokens:,} tokens[/dim]")
|
|
142
|
+
self.ui.console.print(f" [dim] total cost: ${cost:.4f}[/dim]")
|
|
143
|
+
|
|
144
|
+
result: dict[str, Any] = {
|
|
145
|
+
"script_path": script_path,
|
|
146
|
+
"usage": self.usage_metadata,
|
|
147
|
+
}
|
|
148
|
+
self.message_store.save_result(result)
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
self.ui.error(str(e))
|
|
153
|
+
self.message_store.save_error(str(e))
|
|
154
|
+
self.ui.console.print("\n[dim]Make sure Claude Code CLI is installed: npm install -g @anthropic-ai/claude-code[/dim]")
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Keep old class name for backwards compatibility
|
|
161
|
+
APIReverseEngineer = ClaudeEngineer
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_reverse_engineering(
|
|
165
|
+
run_id: str,
|
|
166
|
+
har_path: Path,
|
|
167
|
+
prompt: str,
|
|
168
|
+
model: str | None = None,
|
|
169
|
+
additional_instructions: str | None = None,
|
|
170
|
+
output_dir: str | None = None,
|
|
171
|
+
verbose: bool = True,
|
|
172
|
+
sdk: str = "claude",
|
|
173
|
+
opencode_provider: str | None = None,
|
|
174
|
+
opencode_model: str | None = None,
|
|
175
|
+
copilot_model: str | None = None,
|
|
176
|
+
enable_sync: bool = False,
|
|
177
|
+
is_fresh: bool = False,
|
|
178
|
+
output_language: str = "python",
|
|
179
|
+
output_mode: str = "client",
|
|
180
|
+
) -> dict[str, Any] | None:
|
|
181
|
+
"""Run reverse engineering with the specified SDK.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
sdk: "claude", "opencode", or "copilot" - determines which SDK to use
|
|
185
|
+
opencode_provider: Provider ID for OpenCode (e.g., "anthropic")
|
|
186
|
+
opencode_model: Model ID for OpenCode (e.g., "claude-sonnet-4-6")
|
|
187
|
+
copilot_model: Model ID for Copilot (e.g., "gpt-5")
|
|
188
|
+
enable_sync: Enable real-time file syncing during engineering
|
|
189
|
+
is_fresh: Whether to start fresh (ignore previous scripts)
|
|
190
|
+
output_language: Target language - "python", "javascript", or "typescript"
|
|
191
|
+
output_mode: Output mode - "client" for API client code, "docs" for OpenAPI specification
|
|
192
|
+
"""
|
|
193
|
+
if sdk == "opencode":
|
|
194
|
+
from .opencode_engineer import OpenCodeEngineer
|
|
195
|
+
|
|
196
|
+
engineer = OpenCodeEngineer(
|
|
197
|
+
run_id=run_id,
|
|
198
|
+
har_path=har_path,
|
|
199
|
+
prompt=prompt,
|
|
200
|
+
model=model,
|
|
201
|
+
additional_instructions=additional_instructions,
|
|
202
|
+
output_dir=output_dir,
|
|
203
|
+
verbose=verbose,
|
|
204
|
+
opencode_provider=opencode_provider,
|
|
205
|
+
opencode_model=opencode_model,
|
|
206
|
+
enable_sync=enable_sync,
|
|
207
|
+
sdk=sdk,
|
|
208
|
+
is_fresh=is_fresh,
|
|
209
|
+
output_language=output_language,
|
|
210
|
+
output_mode=output_mode,
|
|
211
|
+
)
|
|
212
|
+
elif sdk == "copilot":
|
|
213
|
+
from .copilot_engineer import CopilotEngineer
|
|
214
|
+
|
|
215
|
+
engineer = CopilotEngineer(
|
|
216
|
+
run_id=run_id,
|
|
217
|
+
har_path=har_path,
|
|
218
|
+
prompt=prompt,
|
|
219
|
+
model=model,
|
|
220
|
+
additional_instructions=additional_instructions,
|
|
221
|
+
output_dir=output_dir,
|
|
222
|
+
verbose=verbose,
|
|
223
|
+
enable_sync=enable_sync,
|
|
224
|
+
sdk=sdk,
|
|
225
|
+
is_fresh=is_fresh,
|
|
226
|
+
output_language=output_language,
|
|
227
|
+
output_mode=output_mode,
|
|
228
|
+
copilot_model=copilot_model,
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
engineer = ClaudeEngineer(
|
|
232
|
+
run_id=run_id,
|
|
233
|
+
har_path=har_path,
|
|
234
|
+
prompt=prompt,
|
|
235
|
+
model=model,
|
|
236
|
+
additional_instructions=additional_instructions,
|
|
237
|
+
output_dir=output_dir,
|
|
238
|
+
verbose=verbose,
|
|
239
|
+
enable_sync=enable_sync,
|
|
240
|
+
sdk=sdk,
|
|
241
|
+
is_fresh=is_fresh,
|
|
242
|
+
output_language=output_language,
|
|
243
|
+
output_mode=output_mode,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Start sync before analysis
|
|
247
|
+
engineer.start_sync()
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
result = asyncio.run(engineer.analyze_and_generate())
|
|
251
|
+
finally:
|
|
252
|
+
# Always stop sync when done
|
|
253
|
+
engineer.stop_sync()
|
|
254
|
+
|
|
255
|
+
return result
|