reverse-api-engineer 0.4.4__tar.gz → 0.5.0__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 (145) hide show
  1. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/CHANGELOG.md +20 -3
  2. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/PKG-INFO +1 -6
  3. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/pyproject.toml +1 -6
  4. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/auto_engineer.py +33 -99
  5. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/base_engineer.py +56 -3
  6. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/cli.py +76 -5
  7. reverse_api_engineer-0.5.0/src/reverse_api/engineer.py +292 -0
  8. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/utils.py +1 -1
  9. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_engineer.py +267 -127
  10. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/uv.lock +1 -159
  11. reverse_api_engineer-0.4.4/src/reverse_api/engineer.py +0 -253
  12. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/.claude/settings.local.json +0 -0
  13. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/.claude-plugin/marketplace.json +0 -0
  14. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/.gitignore +0 -0
  15. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/.python-version +0 -0
  16. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/CLAUDE.md +0 -0
  17. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/CONTRIBUTING.md +0 -0
  18. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/INTERVIEW.md +0 -0
  19. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/LICENSE +0 -0
  20. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/PROMPT_STASH.md +0 -0
  21. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/README.md +0 -0
  22. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/RELEASING.md +0 -0
  23. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/assets/reverse-api-banner.svg +0 -0
  24. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/assets/reverse-api-engineer.gif +0 -0
  25. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/assets/reverse-api-logo.svg +0 -0
  26. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/.claude/settings.local.json +0 -0
  27. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/components.json +0 -0
  28. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/package-lock.json +0 -0
  29. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/package.json +0 -0
  30. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/packed/reverse-api-engineer-chrome.zip +0 -0
  31. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/postcss.config.js +0 -0
  32. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/public/_locales/en/messages.json +0 -0
  33. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/public/icons/icon-128.png +0 -0
  34. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/public/icons/icon-16.png +0 -0
  35. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/public/icons/icon-32.png +0 -0
  36. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/public/icons/icon-48.png +0 -0
  37. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/public/manifest.json +0 -0
  38. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/background/service-worker.ts +0 -0
  39. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/agent-action.tsx +0 -0
  40. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/chat-input.tsx +0 -0
  41. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/icons.tsx +0 -0
  42. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/markdown-renderer.tsx +0 -0
  43. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/mode-selector.tsx +0 -0
  44. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/plan.tsx +0 -0
  45. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/session-selector.tsx +0 -0
  46. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/terminal.tsx +0 -0
  47. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/components/ui/code-block.tsx +0 -0
  48. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/content/codegen-recorder.ts +0 -0
  49. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/index.css +0 -0
  50. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/shared/capture.ts +0 -0
  51. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/shared/native-host.ts +0 -0
  52. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/shared/storage.ts +0 -0
  53. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/shared/types.ts +0 -0
  54. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/sidepanel/index.html +0 -0
  55. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/sidepanel/main.tsx +0 -0
  56. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/sidepanel/side-panel.tsx +0 -0
  57. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/src/vite-env.d.ts +0 -0
  58. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/store-assets/screenshot-1280x800.png +0 -0
  59. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/tailwind.config.js +0 -0
  60. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/tsconfig.json +0 -0
  61. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/chrome-extension/vite.config.ts +0 -0
  62. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/INDEX.md +0 -0
  63. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/QUICKSTART.md +0 -0
  64. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/README.md +0 -0
  65. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/SUMMARY.md +0 -0
  66. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/api_client.py +0 -0
  67. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/extract_job_fields.py +0 -0
  68. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/main.py +0 -0
  69. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/quick_example.py +0 -0
  70. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/apple/requirements.txt +0 -0
  71. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ashby/API_SUMMARY.txt +0 -0
  72. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ashby/QUICKSTART.md +0 -0
  73. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ashby/README.md +0 -0
  74. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ashby/api_client.py +0 -0
  75. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ashby/example_usage.py +0 -0
  76. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ashby/requirements.txt +0 -0
  77. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/autoscout24/README.md +0 -0
  78. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/autoscout24/SUMMARY.md +0 -0
  79. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/autoscout24/api_client.py +0 -0
  80. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ikea/README.md +0 -0
  81. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/ikea/api_client.py +0 -0
  82. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/mintlify/README.md +0 -0
  83. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/mintlify/api_client.py +0 -0
  84. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/uber/API_ANALYSIS_SUMMARY.md +0 -0
  85. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/uber/README.md +0 -0
  86. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/uber/api_client.py +0 -0
  87. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/uber/example_fetch_all_jobs.py +0 -0
  88. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/uber/quick_start.py +0 -0
  89. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/examples/uber/requirements.txt +0 -0
  90. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/llm-docs/OPENCODE_API_SUMMARY.md +0 -0
  91. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/llm-docs/opencode-api.json +0 -0
  92. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/.claude-plugin/plugin.json +0 -0
  93. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/CHANGELOG.md +0 -0
  94. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/LICENSE +0 -0
  95. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/README.md +0 -0
  96. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/agents/api-reverse-engineer.md +0 -0
  97. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/commands/agent.md +0 -0
  98. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/commands/capture.md +0 -0
  99. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/commands/engineer.md +0 -0
  100. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/commands/manual.md +0 -0
  101. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/CHANGELOG.md +0 -0
  102. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/LICENSE +0 -0
  103. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/SKILL.md +0 -0
  104. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/AUTH_PATTERNS.md +0 -0
  105. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/HAR_ANALYSIS.md +0 -0
  106. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_analyze.py +0 -0
  107. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_filter.py +0 -0
  108. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_utils.py +0 -0
  109. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_validate.py +0 -0
  110. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/plugins/reverse-api-engineer/skills/reverse-engineering-api/templates/api_client.py +0 -0
  111. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/scripts/clean_build.sh +0 -0
  112. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/__init__.py +0 -0
  113. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/action_recorder.py +0 -0
  114. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/browser.py +0 -0
  115. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/collector.py +0 -0
  116. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/collector_ui.py +0 -0
  117. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/config.py +0 -0
  118. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/copilot_engineer.py +0 -0
  119. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/messages.py +0 -0
  120. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/native_host.py +0 -0
  121. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/opencode_engineer.py +0 -0
  122. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/opencode_ui.py +0 -0
  123. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/playwright_codegen.py +0 -0
  124. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/pricing.py +0 -0
  125. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/session.py +0 -0
  126. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/sync.py +0 -0
  127. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/src/reverse_api/tui.py +0 -0
  128. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/__init__.py +0 -0
  129. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/conftest.py +0 -0
  130. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_action_recorder.py +0 -0
  131. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_auto_engineer.py +0 -0
  132. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_base_engineer.py +0 -0
  133. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_collector.py +0 -0
  134. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_collector_ui.py +0 -0
  135. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_config.py +0 -0
  136. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_init.py +0 -0
  137. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_messages.py +0 -0
  138. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_native_host.py +0 -0
  139. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_opencode_engineer.py +0 -0
  140. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_opencode_ui.py +0 -0
  141. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_pricing.py +0 -0
  142. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_session.py +0 -0
  143. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_sync.py +0 -0
  144. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_tui.py +0 -0
  145. {reverse_api_engineer-0.4.4 → reverse_api_engineer-0.5.0}/tests/test_utils.py +0 -0
@@ -5,11 +5,23 @@ 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.4] - 2026-03-15
8
+ ## [0.5.0] - 2026-03-17
9
+
10
+ ### Added
11
+ - **Follow-up chat**: After a run completes, type follow-up messages to iterate in the same session without creating new run IDs or folders. Press Enter to finish
12
+ - **Abort run (Ctrl+C)**: Gracefully cancel a running agent/engineer session and return to the REPL instead of exiting the app
13
+ - **AskUserQuestion free mode**: All select/checkbox prompts now include "Other (type your answer)" so users can always provide free-text input. Updated agent prompt to document free-form, multi-select, and multi-question capabilities
14
+ - **Random task suggestions (Ctrl+R)**: Press Ctrl+R in agent mode to fill the prompt with a random curated task idea. Press again to cycle
15
+
16
+ ### Fixed
17
+ - **Usage tracking across follow-ups**: Token counts now accumulate across all turns in a session instead of being overwritten by the last turn
18
+ - **Agent mode follow-up**: Auto engineer (agent mode) was using a copy-pasted streaming loop without follow-up support; now reuses the shared conversation loop
19
+
20
+ ## [0.4.5] - 2026-03-15
9
21
 
10
22
  ### Fixed
11
- - **Stream closed errors (#51)**: Fixed "Error in hook callback hook_0: Stream closed" by clearing inherited `CLAUDECODE` env var from CLI subprocess and extending stream close timeout
12
- - **Async generator cleanup**: Avoid early return inside `query()` loop to prevent cancel-scope errors
23
+ - **Stream closed errors (#51)**: Reverted from `query()` to `ClaudeSDKClient` which maintains a persistent bidirectional connection, eliminating "Error in hook callback hook_0: Stream closed" errors. The original AUQ fix (v0.4.3) unnecessarily switched APIs only the `can_use_tool` callback signature needed updating
24
+ - **CLAUDECODE env var leak**: Clear inherited `CLAUDECODE` env var from CLI subprocess to prevent nested session interference when running inside Claude Code
13
25
  - **CLI stderr noise**: Filter minified JS stack traces into a single clean error line (use `DEBUG=1` for full output)
14
26
 
15
27
  ### Changed
@@ -17,6 +29,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
29
  - **Agent mode**: No longer prompts for URL (agent navigates autonomously)
18
30
  - **Header UI**: Version and task labels now use mode-specific colors (agent=coral, engineer=blue, collector=gold)
19
31
 
32
+ ## [0.4.4] - 2026-03-15
33
+
34
+ ### Fixed
35
+ - **Stream closed errors (#51)**: Partial fix using `query()` with env var workarounds (superseded by v0.4.5)
36
+
20
37
  ## [0.4.3] - 2026-03-12
21
38
 
22
39
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reverse-api-engineer
3
- Version: 0.4.4
3
+ Version: 0.5.0
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
@@ -19,18 +19,13 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Internet :: WWW/HTTP
20
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Requires-Python: >=3.11
22
- Requires-Dist: aiohttp>=3.12.15
23
- Requires-Dist: anthropic>=0.40.0
24
- Requires-Dist: brotli>=1.2.0
25
22
  Requires-Dist: claude-agent-sdk>=0.1.48
26
23
  Requires-Dist: click>=8.1.0
27
24
  Requires-Dist: playwright-stealth>=1.0.0
28
25
  Requires-Dist: playwright>=1.40.0
29
26
  Requires-Dist: questionary>=2.0.0
30
- Requires-Dist: requests>=2.32.5
31
27
  Requires-Dist: rich>=13.0.0
32
28
  Requires-Dist: setproctitle>=1.3.0
33
- Requires-Dist: sprites-py>=0.0.1a1
34
29
  Requires-Dist: watchdog>=3.0.0
35
30
  Provides-Extra: agent
36
31
  Requires-Dist: stagehand; extra == 'agent'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reverse-api-engineer"
3
- version = "0.4.4"
3
+ version = "0.5.0"
4
4
  description = "A tool to capture browser traffic for API reverse engineering"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -27,13 +27,8 @@ dependencies = [
27
27
  "claude-agent-sdk>=0.1.48",
28
28
  "rich>=13.0.0",
29
29
  "questionary>=2.0.0",
30
- "requests>=2.32.5",
31
- "anthropic>=0.40.0",
32
- "brotli>=1.2.0",
33
- "aiohttp>=3.12.15",
34
30
  "watchdog>=3.0.0",
35
31
  "setproctitle>=1.3.0",
36
- "sprites-py>=0.0.1a1",
37
32
  ]
38
33
 
39
34
  [project.optional-dependencies]
@@ -9,16 +9,10 @@ from typing import Any
9
9
 
10
10
  import httpx
11
11
  from claude_agent_sdk import (
12
- AssistantMessage,
13
12
  ClaudeAgentOptions,
14
- HookMatcher,
13
+ ClaudeSDKClient,
15
14
  PermissionResultAllow,
16
- ResultMessage,
17
- TextBlock,
18
15
  ToolPermissionContext,
19
- ToolResultBlock,
20
- ToolUseBlock,
21
- query,
22
16
  )
23
17
 
24
18
  from .engineer import ClaudeEngineer
@@ -241,7 +235,10 @@ Your final response should confirm the files were created and provide a brief su
241
235
  return PermissionResultAllow(updated_input=input_data)
242
236
 
243
237
  async def analyze_and_generate(self) -> dict[str, Any] | None:
244
- """Run auto mode with MCP browser integration."""
238
+ """Run auto mode with MCP browser integration.
239
+
240
+ Reuses _process_streaming_response and follow-up loop from ClaudeEngineer.
241
+ """
245
242
  self.ui.header(self.run_id, self.prompt, self.model, mode="agent")
246
243
  self.ui.start_analysis()
247
244
  self.message_store.save_prompt(self._build_auto_prompt())
@@ -257,105 +254,44 @@ Your final response should confirm the files were created and provide a brief su
257
254
  ],
258
255
  }
259
256
 
260
- # Required: dummy hook keeps the stream open for can_use_tool
261
- async def _dummy_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> dict[str, Any]:
262
- return {"continue_": True}
263
-
264
- async def _prompt_stream():
265
- yield {
266
- "type": "user",
267
- "message": {"role": "user", "content": self._build_auto_prompt()},
268
- }
269
-
270
257
  options = ClaudeAgentOptions(
271
258
  mcp_servers={"playwright": mcp_config},
259
+ permission_mode="bypassPermissions",
272
260
  can_use_tool=self._handle_tool_permission,
273
- hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[_dummy_hook])]},
274
261
  cwd=str(self.scripts_dir.parent.parent), # Project root
275
262
  model=self.model,
276
- env={"CLAUDECODE": "", "CLAUDE_CODE_STREAM_CLOSE_TIMEOUT": "1800000"},
263
+ env={"CLAUDECODE": ""},
277
264
  stderr=self._handle_cli_stderr,
278
265
  )
279
266
 
280
- final_result: dict[str, Any] | None = None
267
+ last_result: dict[str, Any] | None = None
281
268
 
282
269
  try:
283
- # Do not break/return inside this loop — the SDK requires the
284
- # async generator to be fully consumed to avoid cancel-scope errors.
285
- async for message in query(prompt=_prompt_stream(), options=options):
286
- # Check for usage metadata
287
- if hasattr(message, "usage") and isinstance(message.usage, dict):
288
- self.usage_metadata.update(message.usage)
289
-
290
- if isinstance(message, AssistantMessage):
291
- last_tool_name = None
292
- for block in message.content:
293
- if isinstance(block, ToolUseBlock):
294
- last_tool_name = block.name
295
- self.ui.tool_start(block.name, block.input)
296
- self.message_store.save_tool_start(block.name, block.input)
297
- elif isinstance(block, ToolResultBlock):
298
- is_error = block.is_error if block.is_error else False
299
-
300
- # Extract output from ToolResultBlock
301
- output = None
302
- if hasattr(block, "content"):
303
- output = block.content
304
- elif hasattr(block, "result"):
305
- output = block.result
306
- elif hasattr(block, "output"):
307
- output = block.output
308
-
309
- tool_name = last_tool_name or "Tool"
310
- self.ui.tool_result(tool_name, is_error, output)
311
- self.message_store.save_tool_result(tool_name, is_error, str(output) if output else None)
312
- elif isinstance(block, TextBlock):
313
- self.ui.thinking(block.text)
314
- self.message_store.save_thinking(block.text)
315
-
316
- elif isinstance(message, ResultMessage):
317
- if message.is_error:
318
- self.ui.error(message.result or "Unknown error")
319
- self.message_store.save_error(message.result or "Unknown error")
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
- final_result = {
355
- "script_path": script_path,
356
- "usage": self.usage_metadata,
357
- }
358
- self.message_store.save_result(final_result)
270
+ async with ClaudeSDKClient(options=options) as client:
271
+ await client.query(self._build_auto_prompt())
272
+
273
+ # Process initial response
274
+ last_result = await self._process_streaming_response(client)
275
+ if last_result is None:
276
+ return None
277
+
278
+ # Conversation loop: prompt for follow-ups
279
+ while True:
280
+ follow_up = await self._prompt_follow_up()
281
+ if not follow_up:
282
+ return last_result
283
+
284
+ self.ui.console.print()
285
+ self.message_store.save_prompt(follow_up)
286
+ await client.query(follow_up)
287
+
288
+ result = await self._process_streaming_response(client)
289
+ if result is not None:
290
+ last_result = result
291
+
292
+ except KeyboardInterrupt:
293
+ self.ui.console.print("\n [dim]run aborted[/dim]")
294
+ return last_result
359
295
 
360
296
  except Exception as e:
361
297
  error_msg = str(e)
@@ -374,8 +310,6 @@ Your final response should confirm the files were created and provide a brief su
374
310
  self.ui.console.print("\n[dim]Make sure Claude Code CLI is installed: npm install -g @anthropic-ai/claude-code[/dim]")
375
311
  return None
376
312
 
377
- return final_result
378
-
379
313
 
380
314
  class OpenCodeAutoEngineer(OpenCodeEngineer):
381
315
  """Auto mode using OpenCode SDK: Register MCP server dynamically."""
@@ -1,5 +1,6 @@
1
1
  """Abstract base class for API reverse engineering."""
2
2
 
3
+ import asyncio
3
4
  import os
4
5
  from abc import ABC, abstractmethod
5
6
  from pathlib import Path
@@ -15,6 +16,8 @@ from .utils import generate_folder_name, get_docs_dir, get_history_path, get_scr
15
16
 
16
17
  DEBUG = os.environ.get("DEBUG", "0") == "1"
17
18
 
19
+ OTHER_OPTION = "Other (type your answer)"
20
+
18
21
 
19
22
  class BaseEngineer(ABC):
20
23
  """Abstract base class for API reverse engineering implementations."""
@@ -172,6 +175,7 @@ class BaseEngineer(ABC):
172
175
  for opt in options
173
176
  ]
174
177
  if choices:
178
+ choices.append(OTHER_OPTION)
175
179
  selected = await questionary.checkbox(
176
180
  f" > {question_text}",
177
181
  choices=choices,
@@ -188,7 +192,20 @@ class BaseEngineer(ABC):
188
192
  if selected is None:
189
193
  raise KeyboardInterrupt
190
194
 
191
- labels = [s.split(" - ")[0] if " - " in s else s for s in selected]
195
+ has_other = OTHER_OPTION in selected
196
+ labels = [s.split(" - ")[0] if " - " in s else s for s in selected if s != OTHER_OPTION]
197
+
198
+ if has_other:
199
+ other_text = await questionary.text(
200
+ " > Your answer: ",
201
+ qmark="",
202
+ style=questionary.Style([("question", f"fg:{THEME_SECONDARY}")]),
203
+ ).ask_async()
204
+ if other_text is None:
205
+ raise KeyboardInterrupt
206
+ if other_text.strip():
207
+ labels.append(other_text.strip())
208
+
192
209
  answers[question_text] = ", ".join(labels)
193
210
  else:
194
211
  answer = await questionary.text(
@@ -207,6 +224,7 @@ class BaseEngineer(ABC):
207
224
  for opt in options
208
225
  ]
209
226
  if choices:
227
+ choices.append(OTHER_OPTION)
210
228
  answer = await questionary.select(
211
229
  f" > {question_text}",
212
230
  choices=choices,
@@ -222,8 +240,18 @@ class BaseEngineer(ABC):
222
240
  if answer is None:
223
241
  raise KeyboardInterrupt
224
242
 
225
- label = answer.split(" - ")[0] if " - " in answer else answer
226
- answers[question_text] = label
243
+ if answer == OTHER_OPTION:
244
+ answer = await questionary.text(
245
+ " > Your answer: ",
246
+ qmark="",
247
+ style=questionary.Style([("question", f"fg:{THEME_SECONDARY}")]),
248
+ ).ask_async()
249
+ if answer is None:
250
+ raise KeyboardInterrupt
251
+ answers[question_text] = answer.strip()
252
+ else:
253
+ label = answer.split(" - ")[0] if " - " in answer else answer
254
+ answers[question_text] = label
227
255
  else:
228
256
  answer = await questionary.text(
229
257
  f" > {question_text}",
@@ -243,6 +271,23 @@ class BaseEngineer(ABC):
243
271
  self.ui.console.print()
244
272
  return answers
245
273
 
274
+ async def _prompt_follow_up(self) -> str | None:
275
+ """Prompt user for a follow-up message. Returns None to finish.
276
+
277
+ Uses plain input() via executor instead of questionary to avoid
278
+ terminal state issues after the SDK subprocess exits.
279
+ """
280
+ self.ui.console.print()
281
+ self.ui.console.print(f" [{THEME_PRIMARY}]─[/{THEME_PRIMARY}] [dim]type a follow-up or press Enter to finish[/dim]")
282
+ try:
283
+ loop = asyncio.get_event_loop()
284
+ answer = await loop.run_in_executor(None, lambda: input(" > "))
285
+ if not answer or not answer.strip():
286
+ return None
287
+ return answer.strip()
288
+ except (KeyboardInterrupt, EOFError):
289
+ return None
290
+
246
291
  @staticmethod
247
292
  def _get_opt_field(opt: Any, field: str) -> str:
248
293
  """Get a field from an option, supporting both dict and object access."""
@@ -593,6 +638,14 @@ Here is the output directory where you should save your generated files:
593
638
  **IMPORTANT: You have access to the AskUserQuestion tool to ask clarifying questions during your analysis.**
594
639
  Use this tool when you need to clarify functional requirements, prioritize features, choose between implementation approaches, or gather any other information that would help you generate better {task_description}.
595
640
 
641
+ The AskUserQuestion tool supports:
642
+ - **Multiple questions** in a single call (list of question objects)
643
+ - **Single-select** questions with predefined options (user picks one, or types a custom answer)
644
+ - **Multi-select** questions with predefined options (user picks multiple, or adds a custom answer)
645
+ - **Free-form** questions with no options (user types their answer freely)
646
+
647
+ Use free-form questions (empty options list) for open-ended input like URLs, credentials, descriptions, or any question where predefined choices don't make sense.
648
+
596
649
  Your task is to:
597
650
 
598
651
  1. **Read and analyze the HAR file** to understand all API calls that were captured. Look for:
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import random
3
4
  import re
4
5
  from pathlib import Path
5
6
 
@@ -62,6 +63,29 @@ MODE_DESCRIPTIONS = {
62
63
  "collector": "ai-powered data collection",
63
64
  }
64
65
 
66
+ AGENT_TASK_SUGGESTIONS = [
67
+ "Go to github.com/trending and capture the top 10 trending repos' API calls",
68
+ "Navigate to news.ycombinator.com, browse the front page and capture API interactions",
69
+ "Go to weather.com, search for New York weather and capture the forecast API",
70
+ "Visit reddit.com/r/programming, browse posts and capture the Reddit API calls",
71
+ "Go to maps.google.com, search for restaurants near Times Square and capture API calls",
72
+ "Navigate to twitter.com/explore and capture trending topics API interactions",
73
+ "Go to amazon.com, search for 'mechanical keyboard' and capture product search API",
74
+ "Visit spotify.com/search, search for an artist and capture the search API",
75
+ "Navigate to stackoverflow.com, search for 'python async' and capture the search API",
76
+ "Go to npmjs.com, search for 'express' and capture the package registry API",
77
+ "Visit producthunt.com and capture the feed/listing API calls",
78
+ "Go to crunchbase.com and browse company profiles to capture their API",
79
+ "Navigate to linkedin.com/jobs, search for 'software engineer' and capture job search API",
80
+ "Go to airbnb.com, search for stays in Paris and capture the listing search API",
81
+ "Visit imdb.com, search for a movie and capture the title/search API calls",
82
+ "Go to wolframalpha.com, run a query and capture the computation API",
83
+ "Navigate to booking.com, search for hotels in Tokyo and capture the search API",
84
+ "Go to zillow.com, search for homes in San Francisco and capture the listing API",
85
+ "Visit translate.google.com, translate a paragraph and capture the translation API",
86
+ "Go to unsplash.com, search for 'mountains' and capture the photo search API",
87
+ ]
88
+
65
89
 
66
90
  def prompt_interactive_options(
67
91
  prompt: str | None = None,
@@ -217,6 +241,15 @@ def prompt_interactive_options(
217
241
  # If no completion, just move cursor right
218
242
  buff.cursor_right()
219
243
 
244
+ @kb.add("c-r") # Ctrl+R: random task suggestion (agent mode)
245
+ def random_suggestion(event):
246
+ """Fill prompt with a random task suggestion for agent mode."""
247
+ if mode_state["mode"] == "agent":
248
+ suggestion = random.choice(AGENT_TASK_SUGGESTIONS)
249
+ buff = event.app.current_buffer
250
+ buff.text = suggestion
251
+ buff.cursor_position = len(suggestion)
252
+
220
253
  def get_prompt():
221
254
  """Generate prompt with current mode indicator."""
222
255
  mode = mode_state["mode"]
@@ -330,12 +363,20 @@ def prompt_interactive_options(
330
363
  }
331
364
 
332
365
 
333
- @click.group(invoke_without_command=True)
366
+ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
367
+
368
+
369
+ @click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
334
370
  @click.pass_context
335
371
  @click.version_option(version=__version__)
336
372
  def main(ctx: click.Context):
337
- """Reverse API - Capture browser traffic for API reverse engineering."""
373
+ """reverse-api-engineer: reverse engineer apis.
374
+
375
+ Run without a subcommand to start the interactive REPL; use agent, manual,
376
+ or engineer for CLI mode.
377
+ """
338
378
  setproctitle.setproctitle("reverse-api-engineer")
379
+ setproctitle.setthreadtitle("reverse-api-engineer")
339
380
  if ctx.invoked_subcommand is None:
340
381
  repl_loop()
341
382
 
@@ -358,7 +399,7 @@ def repl_loop():
358
399
  model = config_manager.get("claude_code_model", "claude-sonnet-4-6")
359
400
 
360
401
  display_banner(console, sdk=sdk, model=model)
361
- console.print(" [dim]shift+tab to cycle modes: agent | manual | engineer | collector[/dim]")
402
+ console.print(" [dim]shift+tab to cycle modes | ctrl+r for random task (agent)[/dim]")
362
403
  display_footer(console)
363
404
 
364
405
  # Show update message if background check has completed
@@ -413,7 +454,7 @@ def repl_loop():
413
454
  else:
414
455
  # Unknown command - show error and available commands
415
456
  console.print(f" [red]Unknown command:[/red] {cmd}")
416
- console.print(" [dim]Available commands: /settings, /history, /messages, /help, /exit[/dim]")
457
+ console.print(" [dim]Available commands: /settings, /history, /messages, /help, /commands, /exit[/dim]")
417
458
  continue
418
459
 
419
460
  mode = options.get("mode", "agent")
@@ -976,6 +1017,9 @@ def handle_agent_help(mode_color=THEME_PRIMARY):
976
1017
  table.add_row("", "")
977
1018
 
978
1019
  table.add_row("Shift+Tab", "Cycle to other modes (Manual, Engineer).")
1020
+ table.add_row("", "")
1021
+
1022
+ table.add_row("Ctrl+R", "Fill prompt with a random task suggestion.\n[dim]Press multiple times to cycle through ideas.[/dim]")
979
1023
 
980
1024
  console.print(table)
981
1025
  console.print()
@@ -1078,7 +1122,10 @@ def handle_help(mode_color=THEME_PRIMARY):
1078
1122
  )
1079
1123
  commands_table.add_row("", "")
1080
1124
 
1081
- commands_table.add_row("/help", "Show this help message\n[dim]Usage: /help[/dim]")
1125
+ commands_table.add_row(
1126
+ "/help or /commands",
1127
+ "Show this help message\n[dim]Usage: /help[/dim]",
1128
+ )
1082
1129
  commands_table.add_row("", "")
1083
1130
 
1084
1131
  commands_table.add_row("/exit or /quit", "Exit the application\n[dim]Usage: /exit[/dim]")
@@ -1095,6 +1142,7 @@ def handle_help(mode_color=THEME_PRIMARY):
1095
1142
  modes_table.add_row("agent", "Autonomous agent + capture")
1096
1143
  modes_table.add_row("manual", "Full pipeline: browser + reverse engineering")
1097
1144
  modes_table.add_row("engineer", "Reverse engineer only (enter run_id)")
1145
+ modes_table.add_row("collector", "AI-powered data collection")
1098
1146
 
1099
1147
  console.print(" [bold white]Modes[/bold white] [dim]Shift+Tab to cycle[/dim]")
1100
1148
  console.print(modes_table)
@@ -1181,6 +1229,27 @@ def manual(prompt, url, reverse_engineer, model, output_dir):
1181
1229
  run_manual_capture(prompt, url, reverse_engineer, model, output_dir)
1182
1230
 
1183
1231
 
1232
+ @main.command()
1233
+ @click.option("--prompt", "-p", default=None, help="Instruction for the autonomous agent.")
1234
+ @click.option("--url", "-u", default=None, help="Optional starting URL.")
1235
+ @click.option(
1236
+ "--reverse-engineer/--no-engineer",
1237
+ "reverse_engineer",
1238
+ default=True,
1239
+ help="Run reverse engineering after capture.",
1240
+ )
1241
+ @click.option(
1242
+ "--model",
1243
+ "-m",
1244
+ type=click.Choice(["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"]),
1245
+ default=None,
1246
+ )
1247
+ @click.option("--output-dir", "-o", default=None, help="Custom output directory.")
1248
+ def agent(prompt, url, reverse_engineer, model, output_dir):
1249
+ """Run autonomous agent browser session."""
1250
+ run_agent_capture(prompt=prompt, url=url, reverse_engineer=reverse_engineer, model=model, output_dir=output_dir)
1251
+
1252
+
1184
1253
  def run_manual_capture(prompt=None, url=None, reverse_engineer=True, model=None, output_dir=None):
1185
1254
  """Shared logic for manual capture."""
1186
1255
  output_dir = output_dir or config_manager.get("output_dir")
@@ -1521,6 +1590,8 @@ def run_auto_capture(prompt=None, url=None, model=None, output_dir=None):
1521
1590
 
1522
1591
  try:
1523
1592
  result = asyncio.run(engineer.analyze_and_generate())
1593
+ except KeyboardInterrupt:
1594
+ result = None
1524
1595
  finally:
1525
1596
  # Always stop sync when done
1526
1597
  engineer.stop_sync()