reverse-api-engineer 0.4.2__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 (148) hide show
  1. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/.claude/settings.local.json +10 -1
  2. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/.gitignore +3 -1
  3. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/CHANGELOG.md +17 -0
  4. {reverse_api_engineer-0.4.2 → 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.2 → reverse_api_engineer-0.4.4}/pyproject.toml +2 -2
  8. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/auto_engineer.py +112 -100
  9. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/base_engineer.py +35 -10
  10. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/browser.py +16 -15
  11. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/cli.py +12 -26
  12. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/collector.py +2 -2
  13. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/collector_ui.py +3 -2
  14. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/copilot_engineer.py +1 -1
  15. reverse_api_engineer-0.4.4/src/reverse_api/engineer.py +253 -0
  16. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/opencode_engineer.py +3 -3
  17. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/opencode_ui.py +8 -5
  18. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/playwright_codegen.py +27 -24
  19. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/tui.py +9 -7
  20. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/uv.lock +977 -1001
  21. reverse_api_engineer-0.4.2/llm-docs/claude-agent-sdk/QUICKSTART.md +0 -2017
  22. reverse_api_engineer-0.4.2/llm-docs/claude-agent-sdk/TODO_LIST.md +0 -176
  23. reverse_api_engineer-0.4.2/llm-docs/claude-agent-sdk/TOOLS.md +0 -0
  24. reverse_api_engineer-0.4.2/src/reverse_api/engineer.py +0 -253
  25. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/.claude-plugin/marketplace.json +0 -0
  26. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/.python-version +0 -0
  27. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/CLAUDE.md +0 -0
  28. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/CONTRIBUTING.md +0 -0
  29. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/INTERVIEW.md +0 -0
  30. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/LICENSE +0 -0
  31. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/PROMPT_STASH.md +0 -0
  32. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/README.md +0 -0
  33. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/RELEASING.md +0 -0
  34. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/assets/reverse-api-banner.svg +0 -0
  35. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/assets/reverse-api-engineer.gif +0 -0
  36. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/assets/reverse-api-logo.svg +0 -0
  37. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/.claude/settings.local.json +0 -0
  38. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/components.json +0 -0
  39. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/package-lock.json +0 -0
  40. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/package.json +0 -0
  41. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/postcss.config.js +0 -0
  42. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/public/_locales/en/messages.json +0 -0
  43. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-128.png +0 -0
  44. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-16.png +0 -0
  45. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-32.png +0 -0
  46. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/public/icons/icon-48.png +0 -0
  47. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/public/manifest.json +0 -0
  48. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/background/service-worker.ts +0 -0
  49. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/agent-action.tsx +0 -0
  50. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/chat-input.tsx +0 -0
  51. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/icons.tsx +0 -0
  52. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/markdown-renderer.tsx +0 -0
  53. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/mode-selector.tsx +0 -0
  54. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/plan.tsx +0 -0
  55. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/session-selector.tsx +0 -0
  56. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/terminal.tsx +0 -0
  57. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/components/ui/code-block.tsx +0 -0
  58. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/content/codegen-recorder.ts +0 -0
  59. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/index.css +0 -0
  60. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/capture.ts +0 -0
  61. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/native-host.ts +0 -0
  62. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/storage.ts +0 -0
  63. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/shared/types.ts +0 -0
  64. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/sidepanel/index.html +0 -0
  65. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/sidepanel/main.tsx +0 -0
  66. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/sidepanel/side-panel.tsx +0 -0
  67. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/src/vite-env.d.ts +0 -0
  68. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/tailwind.config.js +0 -0
  69. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/tsconfig.json +0 -0
  70. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/chrome-extension/vite.config.ts +0 -0
  71. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/INDEX.md +0 -0
  72. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/QUICKSTART.md +0 -0
  73. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/README.md +0 -0
  74. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/SUMMARY.md +0 -0
  75. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/api_client.py +0 -0
  76. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/extract_job_fields.py +0 -0
  77. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/main.py +0 -0
  78. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/quick_example.py +0 -0
  79. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/apple/requirements.txt +0 -0
  80. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ashby/API_SUMMARY.txt +0 -0
  81. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ashby/QUICKSTART.md +0 -0
  82. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ashby/README.md +0 -0
  83. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ashby/api_client.py +0 -0
  84. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ashby/example_usage.py +0 -0
  85. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ashby/requirements.txt +0 -0
  86. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/autoscout24/README.md +0 -0
  87. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/autoscout24/SUMMARY.md +0 -0
  88. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/autoscout24/api_client.py +0 -0
  89. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ikea/README.md +0 -0
  90. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/ikea/api_client.py +0 -0
  91. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/mintlify/README.md +0 -0
  92. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/mintlify/api_client.py +0 -0
  93. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/uber/API_ANALYSIS_SUMMARY.md +0 -0
  94. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/uber/README.md +0 -0
  95. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/uber/api_client.py +0 -0
  96. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/uber/example_fetch_all_jobs.py +0 -0
  97. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/uber/quick_start.py +0 -0
  98. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/examples/uber/requirements.txt +0 -0
  99. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/llm-docs/OPENCODE_API_SUMMARY.md +0 -0
  100. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/llm-docs/opencode-api.json +0 -0
  101. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/.claude-plugin/plugin.json +0 -0
  102. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/CHANGELOG.md +0 -0
  103. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/LICENSE +0 -0
  104. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/README.md +0 -0
  105. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/agents/api-reverse-engineer.md +0 -0
  106. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/agent.md +0 -0
  107. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/capture.md +0 -0
  108. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/engineer.md +0 -0
  109. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/commands/manual.md +0 -0
  110. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/CHANGELOG.md +0 -0
  111. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/LICENSE +0 -0
  112. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/SKILL.md +0 -0
  113. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/AUTH_PATTERNS.md +0 -0
  114. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/references/HAR_ANALYSIS.md +0 -0
  115. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_analyze.py +0 -0
  116. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_filter.py +0 -0
  117. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_utils.py +0 -0
  118. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/scripts/har_validate.py +0 -0
  119. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/plugins/reverse-api-engineer/skills/reverse-engineering-api/templates/api_client.py +0 -0
  120. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/scripts/clean_build.sh +0 -0
  121. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/__init__.py +0 -0
  122. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/action_recorder.py +0 -0
  123. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/config.py +0 -0
  124. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/messages.py +0 -0
  125. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/native_host.py +0 -0
  126. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/pricing.py +0 -0
  127. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/session.py +0 -0
  128. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/sync.py +0 -0
  129. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/src/reverse_api/utils.py +0 -0
  130. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/__init__.py +0 -0
  131. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/conftest.py +0 -0
  132. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_action_recorder.py +0 -0
  133. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_auto_engineer.py +0 -0
  134. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_base_engineer.py +0 -0
  135. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_collector.py +0 -0
  136. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_collector_ui.py +0 -0
  137. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_config.py +0 -0
  138. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_engineer.py +0 -0
  139. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_init.py +0 -0
  140. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_messages.py +0 -0
  141. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_native_host.py +0 -0
  142. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_opencode_engineer.py +0 -0
  143. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_opencode_ui.py +0 -0
  144. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_pricing.py +0 -0
  145. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_session.py +0 -0
  146. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_sync.py +0 -0
  147. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_tui.py +0 -0
  148. {reverse_api_engineer-0.4.2 → reverse_api_engineer-0.4.4}/tests/test_utils.py +0 -0
@@ -111,7 +111,16 @@
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)",
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)"
115
124
  ]
116
125
  }
117
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,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
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
+
20
+ ## [0.4.3] - 2026-03-12
21
+
22
+ ### Fixed
23
+ - **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
24
+
8
25
  ## [0.4.2] - 2026-03-12
9
26
 
10
27
  ### 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.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.2"
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",
@@ -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,9 +229,20 @@ 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(self, tool_name: str, input_data: dict[str, Any], context: ToolPermissionContext) -> PermissionResultAllow:
233
+ """Handle tool permission requests, with interactive UI for AskUserQuestion."""
234
+ if tool_name == "AskUserQuestion":
235
+ questions = input_data.get("questions", [])
236
+ answers = await self._ask_user_interactive(questions)
237
+ return PermissionResultAllow(
238
+ updated_input={"questions": questions, "answers": answers},
239
+ )
240
+ # Auto-approve all other tools
241
+ return PermissionResultAllow(updated_input=input_data)
242
+
229
243
  async def analyze_and_generate(self) -> dict[str, Any] | None:
230
244
  """Run auto mode with MCP browser integration."""
231
- self.ui.header(self.run_id, self.prompt, self.model)
245
+ self.ui.header(self.run_id, self.prompt, self.model, mode="agent")
232
246
  self.ui.start_analysis()
233
247
  self.message_store.save_prompt(self._build_auto_prompt())
234
248
 
@@ -243,126 +257,124 @@ Your final response should confirm the files were created and provide a brief su
243
257
  ],
244
258
  }
245
259
 
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
+
246
270
  options = ClaudeAgentOptions(
247
271
  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
- ],
272
+ can_use_tool=self._handle_tool_permission,
273
+ hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[_dummy_hook])]},
258
274
  cwd=str(self.scripts_dir.parent.parent), # Project root
259
275
  model=self.model,
276
+ env={"CLAUDECODE": "", "CLAUDE_CODE_STREAM_CLOSE_TIMEOUT": "1800000"},
277
+ stderr=self._handle_cli_stderr,
260
278
  )
261
279
 
280
+ final_result: dict[str, Any] | None = None
281
+
262
282
  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
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)
345
359
 
346
360
  except Exception as e:
347
361
  error_msg = str(e)
348
362
  self.ui.error(error_msg)
349
363
  self.message_store.save_error(error_msg)
350
364
 
351
- # Handle screenshot buffer size errors specifically
352
365
  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]")
366
+ self.ui.console.print("\n[yellow]Screenshot too large (exceeds 1MB limit)[/yellow]")
354
367
  self.ui.console.print("[dim]Tip: The AI should take element-specific screenshots instead of full-page screenshots.[/dim]")
355
368
  self.ui.console.print(
356
369
  "[dim]Consider using browser_snapshot() for accessibility tree information when screenshots aren't needed.[/dim]"
357
370
  )
358
- # Provide helpful error messages
359
371
  elif "MCP server" in error_msg or "npx" in error_msg:
360
372
  self.ui.console.print("\n[dim]Make sure rae-playwright-mcp is installed: npm install -g rae-playwright-mcp[/dim]")
361
373
  else:
362
374
  self.ui.console.print("\n[dim]Make sure Claude Code CLI is installed: npm install -g @anthropic-ai/claude-code[/dim]")
363
375
  return None
364
376
 
365
- return None
377
+ return final_result
366
378
 
367
379
 
368
380
  class OpenCodeAutoEngineer(OpenCodeEngineer):
@@ -391,7 +403,7 @@ class OpenCodeAutoEngineer(OpenCodeEngineer):
391
403
 
392
404
  async def analyze_and_generate(self) -> dict[str, Any] | None:
393
405
  """Run auto mode with OpenCode MCP integration."""
394
- 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")
395
407
  self.opencode_ui.start_analysis()
396
408
  self.message_store.save_prompt(self._build_auto_prompt())
397
409
 
@@ -626,7 +638,7 @@ class CopilotAutoEngineer:
626
638
  return None
627
639
 
628
640
  eng = self._engineer
629
- 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")
630
642
  eng.ui.start_analysis()
631
643
 
632
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}