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.
Files changed (146) hide show
  1. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.claude/settings.local.json +3 -1
  2. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/CHANGELOG.md +5 -0
  3. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/PKG-INFO +1 -1
  4. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/pyproject.toml +1 -1
  5. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/auto_engineer.py +109 -96
  6. reverse_api_engineer-0.4.3/src/reverse_api/engineer.py +255 -0
  7. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/uv.lock +1 -1
  8. reverse_api_engineer-0.4.2/src/reverse_api/engineer.py +0 -253
  9. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.claude-plugin/marketplace.json +0 -0
  10. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.gitignore +0 -0
  11. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/.python-version +0 -0
  12. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/CLAUDE.md +0 -0
  13. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/CONTRIBUTING.md +0 -0
  14. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/INTERVIEW.md +0 -0
  15. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/LICENSE +0 -0
  16. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/PROMPT_STASH.md +0 -0
  17. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/README.md +0 -0
  18. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/RELEASING.md +0 -0
  19. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/assets/reverse-api-banner.svg +0 -0
  20. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/assets/reverse-api-engineer.gif +0 -0
  21. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/assets/reverse-api-logo.svg +0 -0
  22. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/.claude/settings.local.json +0 -0
  23. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/components.json +0 -0
  24. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/package-lock.json +0 -0
  25. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/package.json +0 -0
  26. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/postcss.config.js +0 -0
  27. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/_locales/en/messages.json +0 -0
  28. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-128.png +0 -0
  29. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-16.png +0 -0
  30. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-32.png +0 -0
  31. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/icons/icon-48.png +0 -0
  32. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/public/manifest.json +0 -0
  33. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/background/service-worker.ts +0 -0
  34. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/agent-action.tsx +0 -0
  35. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/chat-input.tsx +0 -0
  36. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/icons.tsx +0 -0
  37. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/markdown-renderer.tsx +0 -0
  38. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/mode-selector.tsx +0 -0
  39. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/plan.tsx +0 -0
  40. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/session-selector.tsx +0 -0
  41. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/terminal.tsx +0 -0
  42. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/components/ui/code-block.tsx +0 -0
  43. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/content/codegen-recorder.ts +0 -0
  44. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/index.css +0 -0
  45. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/capture.ts +0 -0
  46. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/native-host.ts +0 -0
  47. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/storage.ts +0 -0
  48. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/shared/types.ts +0 -0
  49. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/sidepanel/index.html +0 -0
  50. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/sidepanel/main.tsx +0 -0
  51. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/sidepanel/side-panel.tsx +0 -0
  52. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/src/vite-env.d.ts +0 -0
  53. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/tailwind.config.js +0 -0
  54. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/tsconfig.json +0 -0
  55. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/chrome-extension/vite.config.ts +0 -0
  56. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/INDEX.md +0 -0
  57. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/QUICKSTART.md +0 -0
  58. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/README.md +0 -0
  59. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/SUMMARY.md +0 -0
  60. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/api_client.py +0 -0
  61. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/extract_job_fields.py +0 -0
  62. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/main.py +0 -0
  63. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/quick_example.py +0 -0
  64. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/apple/requirements.txt +0 -0
  65. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/API_SUMMARY.txt +0 -0
  66. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/QUICKSTART.md +0 -0
  67. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/README.md +0 -0
  68. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/api_client.py +0 -0
  69. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/example_usage.py +0 -0
  70. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ashby/requirements.txt +0 -0
  71. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/autoscout24/README.md +0 -0
  72. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/autoscout24/SUMMARY.md +0 -0
  73. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/autoscout24/api_client.py +0 -0
  74. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ikea/README.md +0 -0
  75. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/ikea/api_client.py +0 -0
  76. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/mintlify/README.md +0 -0
  77. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/mintlify/api_client.py +0 -0
  78. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/API_ANALYSIS_SUMMARY.md +0 -0
  79. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/README.md +0 -0
  80. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/api_client.py +0 -0
  81. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/example_fetch_all_jobs.py +0 -0
  82. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/quick_start.py +0 -0
  83. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/examples/uber/requirements.txt +0 -0
  84. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/OPENCODE_API_SUMMARY.md +0 -0
  85. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/claude-agent-sdk/QUICKSTART.md +0 -0
  86. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/claude-agent-sdk/TODO_LIST.md +0 -0
  87. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/claude-agent-sdk/TOOLS.md +0 -0
  88. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/llm-docs/opencode-api.json +0 -0
  89. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/.claude-plugin/plugin.json +0 -0
  90. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/CHANGELOG.md +0 -0
  91. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/LICENSE +0 -0
  92. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/README.md +0 -0
  93. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/agents/api-reverse-engineer.md +0 -0
  94. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/agent.md +0 -0
  95. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/capture.md +0 -0
  96. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/engineer.md +0 -0
  97. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/commands/manual.md +0 -0
  98. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/CHANGELOG.md +0 -0
  99. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/LICENSE +0 -0
  100. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/plugins/reverse-api-engineer/skills/reverse-engineering-api/SKILL.md +0 -0
  101. {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
  102. {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
  103. {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
  104. {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
  105. {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
  106. {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
  107. {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
  108. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/scripts/clean_build.sh +0 -0
  109. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/__init__.py +0 -0
  110. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/action_recorder.py +0 -0
  111. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/base_engineer.py +0 -0
  112. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/browser.py +0 -0
  113. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/cli.py +0 -0
  114. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/collector.py +0 -0
  115. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/collector_ui.py +0 -0
  116. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/config.py +0 -0
  117. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/copilot_engineer.py +0 -0
  118. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/messages.py +0 -0
  119. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/native_host.py +0 -0
  120. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/opencode_engineer.py +0 -0
  121. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/opencode_ui.py +0 -0
  122. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/playwright_codegen.py +0 -0
  123. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/pricing.py +0 -0
  124. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/session.py +0 -0
  125. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/sync.py +0 -0
  126. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/tui.py +0 -0
  127. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/src/reverse_api/utils.py +0 -0
  128. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/__init__.py +0 -0
  129. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/conftest.py +0 -0
  130. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_action_recorder.py +0 -0
  131. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_auto_engineer.py +0 -0
  132. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_base_engineer.py +0 -0
  133. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_collector.py +0 -0
  134. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_collector_ui.py +0 -0
  135. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_config.py +0 -0
  136. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_engineer.py +0 -0
  137. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_init.py +0 -0
  138. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_messages.py +0 -0
  139. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_native_host.py +0 -0
  140. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_opencode_engineer.py +0 -0
  141. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_opencode_ui.py +0 -0
  142. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_pricing.py +0 -0
  143. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_session.py +0 -0
  144. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_sync.py +0 -0
  145. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_tui.py +0 -0
  146. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.3}/tests/test_utils.py +0 -0
@@ -111,7 +111,9 @@
111
111
  "Bash(pip show:*)",
112
112
  "Bash(npm uninstall:*)",
113
113
  "Bash(npx rollup-plugin-visualizer:*)",
114
- "Bash(git pull:*)"
114
+ "Bash(git pull:*)",
115
+ "WebFetch(domain:docs.anthropic.com)",
116
+ "WebFetch(domain:docs.claude.com)"
115
117
  ]
116
118
  }
117
119
  }
@@ -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.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reverse-api-engineer"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
4
  description = "A tool to capture browser traffic for API reverse engineering"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -11,11 +11,14 @@ import httpx
11
11
  from claude_agent_sdk import (
12
12
  AssistantMessage,
13
13
  ClaudeAgentOptions,
14
- ClaudeSDKClient,
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
- permission_mode="bypassPermissions", # Auto-accept browser tool usage
249
- allowed_tools=[
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 with ClaudeSDKClient(options=options) as client:
264
- await client.query(self._build_auto_prompt())
265
-
266
- # Process response and show progress with TUI
267
- async for message in client.receive_response():
268
- # Check for usage metadata
269
- if hasattr(message, "usage") and isinstance(message.usage, dict):
270
- self.usage_metadata.update(message.usage)
271
-
272
- if isinstance(message, AssistantMessage):
273
- last_tool_name = None
274
- for block in message.content:
275
- if isinstance(block, ToolUseBlock):
276
- last_tool_name = block.name
277
- self.ui.tool_start(block.name, block.input)
278
- self.message_store.save_tool_start(block.name, block.input)
279
- elif isinstance(block, ToolResultBlock):
280
- is_error = block.is_error if block.is_error else False
281
-
282
- # Extract output from ToolResultBlock
283
- output = None
284
- if hasattr(block, "content"):
285
- output = block.content
286
- elif hasattr(block, "result"):
287
- output = block.result
288
- elif hasattr(block, "output"):
289
- output = block.output
290
-
291
- tool_name = last_tool_name or "Tool"
292
- self.ui.tool_result(tool_name, is_error, output)
293
- self.message_store.save_tool_result(tool_name, is_error, str(output) if output else None)
294
- elif isinstance(block, TextBlock):
295
- self.ui.thinking(block.text)
296
- self.message_store.save_thinking(block.text)
297
-
298
- elif isinstance(message, ResultMessage):
299
- if message.is_error:
300
- self.ui.error(message.result or "Unknown error")
301
- self.message_store.save_error(message.result or "Unknown error")
302
- return None
303
- else:
304
- script_path = str(self.scripts_dir / self._get_client_filename())
305
- local_path = str(self.local_scripts_dir / self._get_client_filename()) if self.local_scripts_dir else None
306
- self.ui.success(script_path, local_path)
307
-
308
- # Calculate estimated cost if we have usage data
309
- if self.usage_metadata:
310
- input_tokens = self.usage_metadata.get("input_tokens", 0)
311
- output_tokens = self.usage_metadata.get("output_tokens", 0)
312
- cache_creation_tokens = self.usage_metadata.get("cache_creation_input_tokens", 0)
313
- cache_read_tokens = self.usage_metadata.get("cache_read_input_tokens", 0)
314
-
315
- # Calculate cost using shared pricing module
316
- from .pricing import calculate_cost
317
-
318
- cost = calculate_cost(
319
- model_id=self.model,
320
- input_tokens=input_tokens,
321
- output_tokens=output_tokens,
322
- cache_creation_tokens=cache_creation_tokens,
323
- cache_read_tokens=cache_read_tokens,
324
- )
325
- self.usage_metadata["estimated_cost_usd"] = cost
326
-
327
- # Display usage breakdown
328
- self.ui.console.print(f" [dim]Usage:[/dim]") # noqa: F541
329
- if input_tokens > 0:
330
- self.ui.console.print(f" [dim] input: {input_tokens:,} tokens[/dim]")
331
- if cache_creation_tokens > 0:
332
- self.ui.console.print(f" [dim] cache creation: {cache_creation_tokens:,} tokens[/dim]")
333
- if cache_read_tokens > 0:
334
- self.ui.console.print(f" [dim] cache read: {cache_read_tokens:,} tokens[/dim]")
335
- if output_tokens > 0:
336
- self.ui.console.print(f" [dim] output: {output_tokens:,} tokens[/dim]")
337
- self.ui.console.print(f" [dim] total cost: ${cost:.4f}[/dim]")
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]Screenshot too large (exceeds 1MB limit)[/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
@@ -2189,7 +2189,7 @@ wheels = [
2189
2189
 
2190
2190
  [[package]]
2191
2191
  name = "reverse-api-engineer"
2192
- version = "0.4.0"
2192
+ version = "0.4.2"
2193
2193
  source = { editable = "." }
2194
2194
  dependencies = [
2195
2195
  { name = "aiohttp" },