reverse-api-engineer 0.4.3__tar.gz → 0.4.4__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 (147) hide show
  1. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/.claude/settings.local.json +8 -1
  2. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/.gitignore +3 -1
  3. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/CHANGELOG.md +12 -0
  4. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/PKG-INFO +2 -2
  5. reverse_api_engineer-0.4.4/chrome-extension/packed/reverse-api-engineer-chrome.zip +0 -0
  6. reverse_api_engineer-0.4.4/chrome-extension/store-assets/screenshot-1280x800.png +0 -0
  7. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/pyproject.toml +2 -2
  8. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/auto_engineer.py +16 -17
  9. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/base_engineer.py +35 -10
  10. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/browser.py +16 -15
  11. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/cli.py +12 -26
  12. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/collector.py +2 -2
  13. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/collector_ui.py +3 -2
  14. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/copilot_engineer.py +1 -1
  15. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/engineer.py +14 -16
  16. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/opencode_engineer.py +3 -3
  17. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/opencode_ui.py +8 -5
  18. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/playwright_codegen.py +27 -24
  19. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/tui.py +9 -7
  20. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/uv.lock +977 -1001
  21. reverse_api_engineer-0.4.3/llm-docs/claude-agent-sdk/QUICKSTART.md +0 -2017
  22. reverse_api_engineer-0.4.3/llm-docs/claude-agent-sdk/TODO_LIST.md +0 -176
  23. reverse_api_engineer-0.4.3/llm-docs/claude-agent-sdk/TOOLS.md +0 -0
  24. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/.claude-plugin/marketplace.json +0 -0
  25. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/.python-version +0 -0
  26. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/CLAUDE.md +0 -0
  27. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/CONTRIBUTING.md +0 -0
  28. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/INTERVIEW.md +0 -0
  29. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/LICENSE +0 -0
  30. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/PROMPT_STASH.md +0 -0
  31. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/README.md +0 -0
  32. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/RELEASING.md +0 -0
  33. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/assets/reverse-api-banner.svg +0 -0
  34. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/assets/reverse-api-engineer.gif +0 -0
  35. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/assets/reverse-api-logo.svg +0 -0
  36. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/.claude/settings.local.json +0 -0
  37. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/components.json +0 -0
  38. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/package-lock.json +0 -0
  39. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/package.json +0 -0
  40. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/postcss.config.js +0 -0
  41. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/public/_locales/en/messages.json +0 -0
  42. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-128.png +0 -0
  43. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-16.png +0 -0
  44. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-32.png +0 -0
  45. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-48.png +0 -0
  46. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/public/manifest.json +0 -0
  47. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/background/service-worker.ts +0 -0
  48. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/agent-action.tsx +0 -0
  49. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/chat-input.tsx +0 -0
  50. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/icons.tsx +0 -0
  51. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/markdown-renderer.tsx +0 -0
  52. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/mode-selector.tsx +0 -0
  53. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/plan.tsx +0 -0
  54. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/session-selector.tsx +0 -0
  55. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/terminal.tsx +0 -0
  56. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/ui/code-block.tsx +0 -0
  57. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/content/codegen-recorder.ts +0 -0
  58. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/index.css +0 -0
  59. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/capture.ts +0 -0
  60. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/native-host.ts +0 -0
  61. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/storage.ts +0 -0
  62. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/types.ts +0 -0
  63. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/sidepanel/index.html +0 -0
  64. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/sidepanel/main.tsx +0 -0
  65. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/sidepanel/side-panel.tsx +0 -0
  66. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/src/vite-env.d.ts +0 -0
  67. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/tailwind.config.js +0 -0
  68. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/tsconfig.json +0 -0
  69. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/chrome-extension/vite.config.ts +0 -0
  70. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/INDEX.md +0 -0
  71. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/QUICKSTART.md +0 -0
  72. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/README.md +0 -0
  73. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/SUMMARY.md +0 -0
  74. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/api_client.py +0 -0
  75. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/extract_job_fields.py +0 -0
  76. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/main.py +0 -0
  77. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/quick_example.py +0 -0
  78. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/apple/requirements.txt +0 -0
  79. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ashby/API_SUMMARY.txt +0 -0
  80. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ashby/QUICKSTART.md +0 -0
  81. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ashby/README.md +0 -0
  82. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ashby/api_client.py +0 -0
  83. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ashby/example_usage.py +0 -0
  84. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ashby/requirements.txt +0 -0
  85. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/autoscout24/README.md +0 -0
  86. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/autoscout24/SUMMARY.md +0 -0
  87. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/autoscout24/api_client.py +0 -0
  88. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ikea/README.md +0 -0
  89. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/ikea/api_client.py +0 -0
  90. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/mintlify/README.md +0 -0
  91. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/mintlify/api_client.py +0 -0
  92. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/uber/API_ANALYSIS_SUMMARY.md +0 -0
  93. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/uber/README.md +0 -0
  94. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/uber/api_client.py +0 -0
  95. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/uber/example_fetch_all_jobs.py +0 -0
  96. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/uber/quick_start.py +0 -0
  97. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/examples/uber/requirements.txt +0 -0
  98. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/llm-docs/OPENCODE_API_SUMMARY.md +0 -0
  99. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/llm-docs/opencode-api.json +0 -0
  100. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/.claude-plugin/plugin.json +0 -0
  101. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/CHANGELOG.md +0 -0
  102. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/LICENSE +0 -0
  103. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/README.md +0 -0
  104. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/agents/api-reverse-engineer.md +0 -0
  105. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/agent.md +0 -0
  106. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/capture.md +0 -0
  107. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/engineer.md +0 -0
  108. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/manual.md +0 -0
  109. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/CHANGELOG.md +0 -0
  110. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/LICENSE +0 -0
  111. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/SKILL.md +0 -0
  112. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/AUTH_PATTERNS.md +0 -0
  113. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/HAR_ANALYSIS.md +0 -0
  114. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_analyze.py +0 -0
  115. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_filter.py +0 -0
  116. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_utils.py +0 -0
  117. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_validate.py +0 -0
  118. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/templates/api_client.py +0 -0
  119. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/scripts/clean_build.sh +0 -0
  120. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/__init__.py +0 -0
  121. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/action_recorder.py +0 -0
  122. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/config.py +0 -0
  123. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/messages.py +0 -0
  124. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/native_host.py +0 -0
  125. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/pricing.py +0 -0
  126. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/session.py +0 -0
  127. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/sync.py +0 -0
  128. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/src/reverse_api/utils.py +0 -0
  129. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/__init__.py +0 -0
  130. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/conftest.py +0 -0
  131. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_action_recorder.py +0 -0
  132. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_auto_engineer.py +0 -0
  133. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_base_engineer.py +0 -0
  134. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_collector.py +0 -0
  135. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_collector_ui.py +0 -0
  136. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_config.py +0 -0
  137. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_engineer.py +0 -0
  138. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_init.py +0 -0
  139. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_messages.py +0 -0
  140. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_native_host.py +0 -0
  141. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_opencode_engineer.py +0 -0
  142. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_opencode_ui.py +0 -0
  143. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_pricing.py +0 -0
  144. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_session.py +0 -0
  145. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_sync.py +0 -0
  146. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_tui.py +0 -0
  147. {reverse_api_engineer-0.4.3 → reverse_api_engineer-0.4.4}/tests/test_utils.py +0 -0
@@ -113,7 +113,14 @@
113
113
  "Bash(npx rollup-plugin-visualizer:*)",
114
114
  "Bash(git pull:*)",
115
115
  "WebFetch(domain:docs.anthropic.com)",
116
- "WebFetch(domain:docs.claude.com)"
116
+ "WebFetch(domain:docs.claude.com)",
117
+ "Bash(uv lock:*)",
118
+ "Bash(cd /Users/kalilbouzigues/Projects/browgents/reverse-api/chrome-extension/dist && zip -r ../packed/reverse-api-engineer-chrome.zip . && ls -lh ../packed/)",
119
+ "mcp__plugin_reverse-api-engineer_rae-playwright-mcp__browser_resize",
120
+ "mcp__plugin_reverse-api-engineer_rae-playwright-mcp__browser_take_screenshot",
121
+ "Bash(gh issue:*)",
122
+ "Bash(CLAUDECODE= uv run python3 /tmp/test_hook.py 2>&1)",
123
+ "Bash(CLAUDECODE=1 uv run python3 /tmp/test_hook4.py 2>&1)"
117
124
  ]
118
125
  }
119
126
  }
@@ -99,4 +99,6 @@ collector_mode_plan.md
99
99
  chrome-extension/node_modules/
100
100
  chrome-extension/dist/
101
101
  chrome-extension-old
102
- debug/
102
+ debug/
103
+
104
+ .playwright-mcp/
@@ -5,6 +5,18 @@ 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
9
+
10
+ ### 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
13
+ - **CLI stderr noise**: Filter minified JS stack traces into a single clean error line (use `DEBUG=1` for full output)
14
+
15
+ ### Changed
16
+ - **claude-agent-sdk**: Bumped minimum version to 0.1.48
17
+ - **Agent mode**: No longer prompts for URL (agent navigates autonomously)
18
+ - **Header UI**: Version and task labels now use mode-specific colors (agent=coral, engineer=blue, collector=gold)
19
+
8
20
  ## [0.4.3] - 2026-03-12
9
21
 
10
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reverse-api-engineer
3
- Version: 0.4.3
3
+ Version: 0.4.4
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
@@ -22,7 +22,7 @@ Requires-Python: >=3.11
22
22
  Requires-Dist: aiohttp>=3.12.15
23
23
  Requires-Dist: anthropic>=0.40.0
24
24
  Requires-Dist: brotli>=1.2.0
25
- Requires-Dist: claude-agent-sdk>=0.1.0
25
+ Requires-Dist: claude-agent-sdk>=0.1.48
26
26
  Requires-Dist: click>=8.1.0
27
27
  Requires-Dist: playwright-stealth>=1.0.0
28
28
  Requires-Dist: playwright>=1.40.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reverse-api-engineer"
3
- version = "0.4.3"
3
+ version = "0.4.4"
4
4
  description = "A tool to capture browser traffic for API reverse engineering"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -24,7 +24,7 @@ dependencies = [
24
24
  "playwright>=1.40.0",
25
25
  "playwright-stealth>=1.0.0",
26
26
  "click>=8.1.0",
27
- "claude-agent-sdk>=0.1.0",
27
+ "claude-agent-sdk>=0.1.48",
28
28
  "rich>=13.0.0",
29
29
  "questionary>=2.0.0",
30
30
  "requests>=2.32.5",
@@ -229,9 +229,7 @@ Your final response should confirm the files were created and provide a brief su
229
229
  - Any limitations or caveats
230
230
  """
231
231
 
232
- async def _handle_tool_permission(
233
- self, tool_name: str, input_data: dict[str, Any], context: ToolPermissionContext
234
- ) -> PermissionResultAllow:
232
+ async def _handle_tool_permission(self, tool_name: str, input_data: dict[str, Any], context: ToolPermissionContext) -> PermissionResultAllow:
235
233
  """Handle tool permission requests, with interactive UI for AskUserQuestion."""
236
234
  if tool_name == "AskUserQuestion":
237
235
  questions = input_data.get("questions", [])
@@ -244,7 +242,7 @@ Your final response should confirm the files were created and provide a brief su
244
242
 
245
243
  async def analyze_and_generate(self) -> dict[str, Any] | None:
246
244
  """Run auto mode with MCP browser integration."""
247
- self.ui.header(self.run_id, self.prompt, self.model)
245
+ self.ui.header(self.run_id, self.prompt, self.model, mode="agent")
248
246
  self.ui.start_analysis()
249
247
  self.message_store.save_prompt(self._build_auto_prompt())
250
248
 
@@ -259,17 +257,14 @@ Your final response should confirm the files were created and provide a brief su
259
257
  ],
260
258
  }
261
259
 
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
260
+ # Required: dummy hook keeps the stream open for can_use_tool
264
261
  async def _dummy_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> dict[str, Any]:
265
262
  return {"continue_": True}
266
263
 
267
- prompt_text = self._build_auto_prompt()
268
-
269
- async def _prompt_stream() -> Any:
264
+ async def _prompt_stream():
270
265
  yield {
271
266
  "type": "user",
272
- "message": {"role": "user", "content": prompt_text},
267
+ "message": {"role": "user", "content": self._build_auto_prompt()},
273
268
  }
274
269
 
275
270
  options = ClaudeAgentOptions(
@@ -278,9 +273,15 @@ Your final response should confirm the files were created and provide a brief su
278
273
  hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[_dummy_hook])]},
279
274
  cwd=str(self.scripts_dir.parent.parent), # Project root
280
275
  model=self.model,
276
+ env={"CLAUDECODE": "", "CLAUDE_CODE_STREAM_CLOSE_TIMEOUT": "1800000"},
277
+ stderr=self._handle_cli_stderr,
281
278
  )
282
279
 
280
+ final_result: dict[str, Any] | None = None
281
+
283
282
  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.
284
285
  async for message in query(prompt=_prompt_stream(), options=options):
285
286
  # Check for usage metadata
286
287
  if hasattr(message, "usage") and isinstance(message.usage, dict):
@@ -316,7 +317,6 @@ Your final response should confirm the files were created and provide a brief su
316
317
  if message.is_error:
317
318
  self.ui.error(message.result or "Unknown error")
318
319
  self.message_store.save_error(message.result or "Unknown error")
319
- return None
320
320
  else:
321
321
  script_path = str(self.scripts_dir / self._get_client_filename())
322
322
  local_path = str(self.local_scripts_dir / self._get_client_filename()) if self.local_scripts_dir else None
@@ -351,12 +351,11 @@ Your final response should confirm the files were created and provide a brief su
351
351
  self.ui.console.print(f" [dim] output: {output_tokens:,} tokens[/dim]")
352
352
  self.ui.console.print(f" [dim] total cost: ${cost:.4f}[/dim]")
353
353
 
354
- result: dict[str, Any] = {
354
+ final_result = {
355
355
  "script_path": script_path,
356
356
  "usage": self.usage_metadata,
357
357
  }
358
- self.message_store.save_result(result)
359
- return result
358
+ self.message_store.save_result(final_result)
360
359
 
361
360
  except Exception as e:
362
361
  error_msg = str(e)
@@ -375,7 +374,7 @@ Your final response should confirm the files were created and provide a brief su
375
374
  self.ui.console.print("\n[dim]Make sure Claude Code CLI is installed: npm install -g @anthropic-ai/claude-code[/dim]")
376
375
  return None
377
376
 
378
- return None
377
+ return final_result
379
378
 
380
379
 
381
380
  class OpenCodeAutoEngineer(OpenCodeEngineer):
@@ -404,7 +403,7 @@ class OpenCodeAutoEngineer(OpenCodeEngineer):
404
403
 
405
404
  async def analyze_and_generate(self) -> dict[str, Any] | None:
406
405
  """Run auto mode with OpenCode MCP integration."""
407
- self.opencode_ui.header(self.run_id, self.prompt, self.opencode_model)
406
+ self.opencode_ui.header(self.run_id, self.prompt, self.opencode_model, mode="agent")
408
407
  self.opencode_ui.start_analysis()
409
408
  self.message_store.save_prompt(self._build_auto_prompt())
410
409
 
@@ -639,7 +638,7 @@ class CopilotAutoEngineer:
639
638
  return None
640
639
 
641
640
  eng = self._engineer
642
- eng.ui.header(eng.run_id, eng.prompt, eng.copilot_model, eng.sdk)
641
+ eng.ui.header(eng.run_id, eng.prompt, eng.copilot_model, eng.sdk, mode="agent")
643
642
  eng.ui.start_analysis()
644
643
 
645
644
  auto_prompt = ClaudeAutoEngineer._build_auto_prompt(eng)
@@ -1,5 +1,6 @@
1
1
  """Abstract base class for API reverse engineering."""
2
2
 
3
+ import os
3
4
  from abc import ABC, abstractmethod
4
5
  from pathlib import Path
5
6
  from typing import Any
@@ -12,6 +13,8 @@ from .sync import FileSyncWatcher, get_available_directory
12
13
  from .tui import THEME_PRIMARY, THEME_SECONDARY, ClaudeUI
13
14
  from .utils import generate_folder_name, get_docs_dir, get_history_path, get_scripts_dir
14
15
 
16
+ DEBUG = os.environ.get("DEBUG", "0") == "1"
17
+
15
18
 
16
19
  class BaseEngineer(ABC):
17
20
  """Abstract base class for API reverse engineering implementations."""
@@ -60,6 +63,24 @@ class BaseEngineer(ABC):
60
63
  self.existing_client_path = self._get_existing_client_path()
61
64
  self.sync_watcher: FileSyncWatcher | None = None
62
65
  self.local_scripts_dir: Path | None = None
66
+ self._stderr_error_shown = False
67
+
68
+ def _handle_cli_stderr(self, line: str) -> None:
69
+ """Filter CLI subprocess stderr. Shows full output in DEBUG mode, otherwise shows a single clean error."""
70
+ if DEBUG:
71
+ self.ui.console.print(f"[dim] stderr: {line.rstrip()}[/dim]")
72
+ return
73
+
74
+ # Known noisy errors from the CLI control protocol — show once
75
+ if "Error in hook callback" in line or "Stream closed" in line:
76
+ if not self._stderr_error_shown:
77
+ self._stderr_error_shown = True
78
+ self.ui.console.print(" [dim]![/dim] [dim]cli stream error (set DEBUG=1 for details)[/dim]")
79
+ return
80
+
81
+ # Suppress other common noise (stack traces, source maps)
82
+ if line.startswith(" at ") or "| " in line[:20]:
83
+ return
63
84
 
64
85
  def start_sync(self):
65
86
  """Start real-time file sync if enabled."""
@@ -532,16 +553,20 @@ Your OpenAPI spec should be production-ready and suitable for:
532
553
  mode_description = f"reverse engineer API calls and generate production-ready {language_name} code that replicates"
533
554
  task_description = f"{language_name} API client"
534
555
 
535
- attempt_log_section = "" if self.output_mode == "docs" else (
536
- "If your first attempt doesn't work, analyze what went wrong and try again. "
537
- "Document each attempt and what you learned.\n\n"
538
- "<attempt_log>\n"
539
- "For each attempt (up to 5), document:\n"
540
- "- Attempt number\n"
541
- "- What approach you tried\n"
542
- "- What error or issue occurred (if any)\n"
543
- "- What you changed for the next attempt\n"
544
- "</attempt_log>\n\n"
556
+ attempt_log_section = (
557
+ ""
558
+ if self.output_mode == "docs"
559
+ else (
560
+ "If your first attempt doesn't work, analyze what went wrong and try again. "
561
+ "Document each attempt and what you learned.\n\n"
562
+ "<attempt_log>\n"
563
+ "For each attempt (up to 5), document:\n"
564
+ "- Attempt number\n"
565
+ "- What approach you tried\n"
566
+ "- What error or issue occurred (if any)\n"
567
+ "- What you changed for the next attempt\n"
568
+ "</attempt_log>\n\n"
569
+ )
545
570
  )
546
571
  after_verb = "documenting" if self.output_mode == "docs" else "testing"
547
572
  output_type = "spec" if self.output_mode == "docs" else "code"
@@ -210,11 +210,11 @@ class ManualBrowser:
210
210
  self.output_dir = output_dir
211
211
  self.use_real_chrome = use_real_chrome
212
212
  self.enable_action_recording = enable_action_recording
213
-
213
+
214
214
  self.har_dir = get_har_dir(run_id, output_dir)
215
215
  self.har_path = self.har_dir / "recording.har"
216
216
  self.metadata_path = self.har_dir / "metadata.json"
217
-
217
+
218
218
  self._playwright = None
219
219
  self._browser: Browser | None = None
220
220
  self._context: BrowserContext | None = None
@@ -226,15 +226,15 @@ class ManualBrowser:
226
226
 
227
227
  def _inject_action_recorder(self, page: Page) -> None:
228
228
  """Inject action recording script into page.
229
-
229
+
230
230
  Uses console.log + page.on('console') for reliable capture.
231
231
  Works best with stealth Chromium mode (not real Chrome).
232
232
  """
233
233
  if not self.enable_action_recording:
234
234
  return
235
-
235
+
236
236
  # Simple JS that logs actions to console with a special prefix
237
- recorder_js = '''
237
+ recorder_js = """
238
238
  window.__recordedActions = [];
239
239
  window.__lastUrl = null;
240
240
 
@@ -378,34 +378,35 @@ class ManualBrowser:
378
378
  }
379
379
  }
380
380
  }
381
- '''
382
-
381
+ """
382
+
383
383
  # Listen to console for actions
384
384
  import json
385
+
385
386
  last_url = [None] # Mutable to track last URL
386
-
387
+
387
388
  def on_console(msg):
388
389
  text = msg.text
389
- if text.startswith('__ACTION__'):
390
+ if text.startswith("__ACTION__"):
390
391
  try:
391
392
  action_json = text[10:] # Remove '__ACTION__' prefix
392
393
  action_data = json.loads(action_json)
393
-
394
+
394
395
  # Filter duplicate navigations
395
- if action_data.get('type') == 'navigate':
396
- url = action_data.get('url', '')
396
+ if action_data.get("type") == "navigate":
397
+ url = action_data.get("url", "")
397
398
  if url == last_url[0]:
398
399
  return # Skip duplicate
399
400
  last_url[0] = url
400
-
401
+
401
402
  if self.action_recorder:
402
403
  self.action_recorder.add_action(RecordedAction(**action_data))
403
404
  except Exception as e:
404
405
  console.print(f" [dim]action parse error: {e}[/dim]")
405
-
406
+
406
407
  page.on("console", on_console)
407
408
  page.add_init_script(recorder_js)
408
-
409
+
409
410
  console.print(" [dim]action recording enabled[/dim]")
410
411
 
411
412
  def _save_metadata(self, end_time: str) -> None:
@@ -54,7 +54,7 @@ config_manager = ConfigManager(get_config_path())
54
54
  session_manager = SessionManager(get_history_path())
55
55
 
56
56
  # Mode definitions
57
- MODES = ["manual", "engineer", "agent", "collector"]
57
+ MODES = ["agent", "manual", "engineer", "collector"]
58
58
  MODE_DESCRIPTIONS = {
59
59
  "manual": "full pipeline",
60
60
  "engineer": "reverse engineer only",
@@ -68,7 +68,7 @@ def prompt_interactive_options(
68
68
  url: str | None = None,
69
69
  reverse_engineer: bool | None = None,
70
70
  model: str | None = None,
71
- current_mode: str = "manual",
71
+ current_mode: str = "agent",
72
72
  ) -> dict:
73
73
  """Prompt user for essential options interactively (Browgents style).
74
74
 
@@ -267,29 +267,15 @@ def prompt_interactive_options(
267
267
  "model": model or config_manager.get("claude_code_model", "claude-sonnet-4-6"),
268
268
  }
269
269
 
270
- # Agent mode: similar to manual but uses autonomous browser
270
+ # Agent mode: autonomous browser, no URL needed (agent navigates on its own)
271
271
  if result_mode == "agent":
272
- if url is None:
273
- try:
274
- url = questionary.text(
275
- " > url",
276
- instruction="(Enter for none)",
277
- qmark="",
278
- style=questionary.Style(
279
- [
280
- ("question", f"fg:{THEME_SECONDARY}"),
281
- ("instruction", f"fg:{THEME_DIM} italic"),
282
- ]
283
- ),
284
- ).ask()
285
- if url is None: # questionary returns None on Ctrl+C
286
- raise click.Abort()
287
- except KeyboardInterrupt:
288
- raise click.Abort()
289
-
290
272
  if model is None:
291
273
  model = config_manager.get("claude_code_model", "claude-sonnet-4-6")
292
274
 
275
+ mode_color = MODE_COLORS.get("agent", THEME_PRIMARY)
276
+ console = Console()
277
+ console.print(f" [{mode_color}]autonomous[/{mode_color}] [dim]agent will navigate on its own[/dim]")
278
+
293
279
  return {
294
280
  "mode": result_mode,
295
281
  "prompt": prompt,
@@ -372,7 +358,7 @@ def repl_loop():
372
358
  model = config_manager.get("claude_code_model", "claude-sonnet-4-6")
373
359
 
374
360
  display_banner(console, sdk=sdk, model=model)
375
- console.print(" [dim]shift+tab to cycle modes: manual | engineer | agent | collector[/dim]")
361
+ console.print(" [dim]shift+tab to cycle modes: agent | manual | engineer | collector[/dim]")
376
362
  display_footer(console)
377
363
 
378
364
  # Show update message if background check has completed
@@ -386,14 +372,14 @@ def repl_loop():
386
372
  finally:
387
373
  update_executor.shutdown(wait=False)
388
374
 
389
- current_mode = "manual"
375
+ current_mode = "agent"
390
376
 
391
377
  while True:
392
378
  try:
393
379
  options = prompt_interactive_options(current_mode=current_mode)
394
380
 
395
381
  # Update current mode for next iteration
396
- current_mode = options.get("mode", "manual")
382
+ current_mode = options.get("mode", "agent")
397
383
 
398
384
  if "command" in options:
399
385
  cmd = options["command"]
@@ -430,7 +416,7 @@ def repl_loop():
430
416
  console.print(" [dim]Available commands: /settings, /history, /messages, /help, /exit[/dim]")
431
417
  continue
432
418
 
433
- mode = options.get("mode", "manual")
419
+ mode = options.get("mode", "agent")
434
420
 
435
421
  # Handle different modes
436
422
  if mode == "engineer":
@@ -1106,9 +1092,9 @@ def handle_help(mode_color=THEME_PRIMARY):
1106
1092
  modes_table.add_column(style=f"{mode_color} bold", justify="left", width=15)
1107
1093
  modes_table.add_column(style="dim", justify="left")
1108
1094
 
1095
+ modes_table.add_row("agent", "Autonomous agent + capture")
1109
1096
  modes_table.add_row("manual", "Full pipeline: browser + reverse engineering")
1110
1097
  modes_table.add_row("engineer", "Reverse engineer only (enter run_id)")
1111
- modes_table.add_row("agent", "Autonomous agent + capture")
1112
1098
 
1113
1099
  console.print(" [bold white]Modes[/bold white] [dim]Shift+Tab to cycle[/dim]")
1114
1100
  console.print(modes_table)
@@ -66,7 +66,7 @@ class Collector:
66
66
  Returns:
67
67
  Result dict with output_path and collected_items, or None on error
68
68
  """
69
- self.ui.header(self.run_id, self.prompt, self.model)
69
+ self.ui.header(self.run_id, self.prompt, self.model, mode="collector")
70
70
  self.ui.start_collecting()
71
71
 
72
72
  self._folder_name = generate_folder_name(self.prompt)
@@ -288,7 +288,7 @@ When complete, briefly summarize what was collected.
288
288
  def _export_readme(self, readme_path: Path, items: list[dict[str, Any]], sources: set[str]) -> None:
289
289
  """Generate README with collection metadata."""
290
290
  folder_name = self._folder_name or "collection"
291
- readme_content = f"""# {folder_name.replace('_', ' ').title()}
291
+ readme_content = f"""# {folder_name.replace("_", " ").title()}
292
292
 
293
293
  ## Query
294
294
  {self.prompt}
@@ -15,12 +15,12 @@ class CollectorUI:
15
15
  self.verbose = verbose
16
16
  self._items_collected = 0
17
17
 
18
- def header(self, run_id: str, prompt: str, model: str | None = None) -> None:
18
+ def header(self, run_id: str, prompt: str, model: str | None = None, **kwargs) -> None:
19
19
  """Display the collector session header."""
20
20
  from . import __version__
21
21
 
22
22
  self.console.print()
23
- self.console.print(f" [white]reverse-api[/white] [dim]v{__version__}[/dim]")
23
+ self.console.print(f" [white]reverse-api[/white] [{COLLECTOR_COLOR}]v{__version__}[/{COLLECTOR_COLOR}]")
24
24
  self.console.print(f" [dim]━[/dim] [white]{run_id}[/white]")
25
25
  self.console.print(f" [{COLLECTOR_COLOR}]collector[/{COLLECTOR_COLOR}] [dim]|[/dim] [dim]model[/dim] [white]{model or '---'}[/white]")
26
26
  self.console.print(f" [{COLLECTOR_COLOR}]task[/{COLLECTOR_COLOR}] [white]{prompt[:80]}{'...' if len(prompt) > 80 else ''}[/white]")
@@ -86,6 +86,7 @@ class CollectorUI:
86
86
  def error(self, message: str) -> None:
87
87
  """Display error message."""
88
88
  from .tui import ERROR_CTA
89
+
89
90
  self.console.print()
90
91
  self.console.print(f" [dim]![/dim] [red]error:[/red] {message}")
91
92
  self.console.print(f" [dim]{ERROR_CTA}[/dim]")
@@ -69,7 +69,7 @@ class CopilotEngineer(BaseEngineer):
69
69
  )
70
70
  return None
71
71
 
72
- self.ui.header(self.run_id, self.prompt, self.copilot_model, self.sdk)
72
+ self.ui.header(self.run_id, self.prompt, self.copilot_model, self.sdk, mode="engineer")
73
73
  self.ui.start_analysis()
74
74
 
75
75
  prompt = self._build_analysis_prompt()
@@ -28,9 +28,7 @@ logging.getLogger("claude_agent_sdk._internal.transport.subprocess_cli").setLeve
28
28
  class ClaudeEngineer(BaseEngineer):
29
29
  """Uses Claude Agent SDK to analyze HAR files and generate Python API scripts."""
30
30
 
31
- async def _handle_tool_permission(
32
- self, tool_name: str, input_data: dict[str, Any], context: ToolPermissionContext
33
- ) -> PermissionResultAllow:
31
+ async def _handle_tool_permission(self, tool_name: str, input_data: dict[str, Any], context: ToolPermissionContext) -> PermissionResultAllow:
34
32
  """Handle tool permission requests, with interactive UI for AskUserQuestion."""
35
33
  if tool_name == "AskUserQuestion":
36
34
  questions = input_data.get("questions", [])
@@ -44,21 +42,19 @@ class ClaudeEngineer(BaseEngineer):
44
42
 
45
43
  async def analyze_and_generate(self) -> dict[str, Any] | None:
46
44
  """Run the reverse engineering analysis with Claude."""
47
- self.ui.header(self.run_id, self.prompt, self.model, self.sdk)
45
+ self.ui.header(self.run_id, self.prompt, self.model, self.sdk, mode="engineer")
48
46
  self.ui.start_analysis()
49
47
  self.message_store.save_prompt(self._build_analysis_prompt())
50
48
 
51
- # Required workaround: dummy hook keeps the stream open for can_use_tool
49
+ # Required: dummy hook keeps the stream open for can_use_tool
52
50
  # See: https://platform.claude.com/docs/en/agent-sdk/user-input
53
51
  async def _dummy_hook(input_data: dict[str, Any], tool_use_id: str | None, context: Any) -> dict[str, Any]:
54
52
  return {"continue_": True}
55
53
 
56
- prompt_text = self._build_analysis_prompt()
57
-
58
- async def _prompt_stream() -> Any:
54
+ async def _prompt_stream():
59
55
  yield {
60
56
  "type": "user",
61
- "message": {"role": "user", "content": prompt_text},
57
+ "message": {"role": "user", "content": self._build_analysis_prompt()},
62
58
  }
63
59
 
64
60
  options = ClaudeAgentOptions(
@@ -66,9 +62,15 @@ class ClaudeEngineer(BaseEngineer):
66
62
  hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[_dummy_hook])]},
67
63
  cwd=str(self.scripts_dir.parent.parent), # Project root
68
64
  model=self.model,
65
+ env={"CLAUDECODE": "", "CLAUDE_CODE_STREAM_CLOSE_TIMEOUT": "1800000"},
66
+ stderr=self._handle_cli_stderr,
69
67
  )
70
68
 
69
+ final_result: dict[str, Any] | None = None
70
+
71
71
  try:
72
+ # Do not break/return inside this loop — the SDK requires the
73
+ # async generator to be fully consumed to avoid cancel-scope errors.
72
74
  async for message in query(prompt=_prompt_stream(), options=options):
73
75
  # Check for usage metadata in message if applicable
74
76
  if hasattr(message, "usage") and isinstance(message.usage, dict):
@@ -104,7 +106,6 @@ class ClaudeEngineer(BaseEngineer):
104
106
  if message.is_error:
105
107
  self.ui.error(message.result or "Unknown error")
106
108
  self.message_store.save_error(message.result or "Unknown error")
107
- return None
108
109
  else:
109
110
  script_path = str(self.scripts_dir / self._get_client_filename())
110
111
  local_path = str(self.local_scripts_dir / self._get_client_filename()) if self.local_scripts_dir else None
@@ -117,7 +118,6 @@ class ClaudeEngineer(BaseEngineer):
117
118
  cache_creation_tokens = self.usage_metadata.get("cache_creation_input_tokens", 0)
118
119
  cache_read_tokens = self.usage_metadata.get("cache_read_input_tokens", 0)
119
120
 
120
- # Calculate cost using shared pricing module
121
121
  from .pricing import calculate_cost
122
122
 
123
123
  cost = calculate_cost(
@@ -129,7 +129,6 @@ class ClaudeEngineer(BaseEngineer):
129
129
  )
130
130
  self.usage_metadata["estimated_cost_usd"] = cost
131
131
 
132
- # Display usage breakdown
133
132
  self.ui.console.print(" [dim]Usage:[/dim]")
134
133
  if input_tokens > 0:
135
134
  self.ui.console.print(f" [dim] input: {input_tokens:,} tokens[/dim]")
@@ -141,12 +140,11 @@ class ClaudeEngineer(BaseEngineer):
141
140
  self.ui.console.print(f" [dim] output: {output_tokens:,} tokens[/dim]")
142
141
  self.ui.console.print(f" [dim] total cost: ${cost:.4f}[/dim]")
143
142
 
144
- result: dict[str, Any] = {
143
+ final_result = {
145
144
  "script_path": script_path,
146
145
  "usage": self.usage_metadata,
147
146
  }
148
- self.message_store.save_result(result)
149
- return result
147
+ self.message_store.save_result(final_result)
150
148
 
151
149
  except Exception as e:
152
150
  self.ui.error(str(e))
@@ -154,7 +152,7 @@ class ClaudeEngineer(BaseEngineer):
154
152
  self.ui.console.print("\n[dim]Make sure Claude Code CLI is installed: npm install -g @anthropic-ai/claude-code[/dim]")
155
153
  return None
156
154
 
157
- return None
155
+ return final_result
158
156
 
159
157
 
160
158
  # Keep old class name for backwards compatibility
@@ -136,7 +136,7 @@ class OpenCodeEngineer(BaseEngineer):
136
136
 
137
137
  async def analyze_and_generate(self) -> dict[str, Any] | None:
138
138
  """Run the reverse engineering analysis with OpenCode."""
139
- self.opencode_ui.header(self.run_id, self.prompt, self.opencode_model, self.sdk)
139
+ self.opencode_ui.header(self.run_id, self.prompt, self.opencode_model, self.sdk, mode="engineer")
140
140
  self.opencode_ui.start_analysis()
141
141
 
142
142
  # Save the prompt to messages
@@ -555,7 +555,7 @@ class OpenCodeEngineer(BaseEngineer):
555
555
  if part_type == "text":
556
556
  text = part.get("text", "")
557
557
  debug_log(f"Handling text part: id={part_id}, delta={'yes' if delta else 'no'}, len={len(text)}")
558
-
558
+
559
559
  # Filter out known prompt text patterns that get echoed back
560
560
  # This prevents the tag context section from appearing in streaming output
561
561
  # Check for the specific tag context pattern that appears at the end of prompts
@@ -563,7 +563,7 @@ class OpenCodeEngineer(BaseEngineer):
563
563
  if tag_context_pattern in text and "Note: Full message history is available" in text:
564
564
  debug_log(f"Filtering out echoed tag context from streaming output")
565
565
  return
566
-
566
+
567
567
  # Use delta for incremental updates if available
568
568
  self.opencode_ui.update_text(text, delta)
569
569