screenforge 0.4.0__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 (107) hide show
  1. screenforge-0.5.0/PKG-INFO +219 -0
  2. {screenforge-0.4.0 → screenforge-0.5.0}/README.md +15 -13
  3. screenforge-0.5.0/cli/_version.py +1 -0
  4. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/action.py +13 -0
  5. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/workflow.py +15 -0
  6. {screenforge-0.4.0 → screenforge-0.5.0}/cli/parser.py +15 -1
  7. screenforge-0.5.0/cli/playground_sink.py +202 -0
  8. {screenforge-0.4.0 → screenforge-0.5.0}/pyproject.toml +2 -1
  9. screenforge-0.5.0/screenforge.egg-info/PKG-INFO +219 -0
  10. {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/SOURCES.txt +4 -0
  11. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_parser.py +12 -0
  12. screenforge-0.5.0/tests/test_playground_app.py +313 -0
  13. screenforge-0.5.0/tests/test_playground_sink.py +423 -0
  14. screenforge-0.5.0/tests/test_playground_sink_integration.py +202 -0
  15. screenforge-0.4.0/PKG-INFO +0 -43
  16. screenforge-0.4.0/cli/_version.py +0 -1
  17. screenforge-0.4.0/screenforge.egg-info/PKG-INFO +0 -43
  18. {screenforge-0.4.0 → screenforge-0.5.0}/LICENSE +0 -0
  19. {screenforge-0.4.0 → screenforge-0.5.0}/cli/__init__.py +0 -0
  20. {screenforge-0.4.0 → screenforge-0.5.0}/cli/dispatch.py +0 -0
  21. {screenforge-0.4.0 → screenforge-0.5.0}/cli/doctor.py +0 -0
  22. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/__init__.py +0 -0
  23. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/default.py +0 -0
  24. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/demo.py +0 -0
  25. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/dry_run.py +0 -0
  26. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/init.py +0 -0
  27. {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/plan.py +0 -0
  28. {screenforge-0.4.0 → screenforge-0.5.0}/cli/reporter.py +0 -0
  29. {screenforge-0.4.0 → screenforge-0.5.0}/cli/session.py +0 -0
  30. {screenforge-0.4.0 → screenforge-0.5.0}/cli/shared.py +0 -0
  31. {screenforge-0.4.0 → screenforge-0.5.0}/cli/shorthand.py +0 -0
  32. {screenforge-0.4.0 → screenforge-0.5.0}/cli/tool_protocol_handlers.py +0 -0
  33. {screenforge-0.4.0 → screenforge-0.5.0}/common/__init__.py +0 -0
  34. {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/__init__.py +0 -0
  35. {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/android_adapter.py +0 -0
  36. {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/base_adapter.py +0 -0
  37. {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/ios_adapter.py +0 -0
  38. {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/web_adapter.py +0 -0
  39. {screenforge-0.4.0 → screenforge-0.5.0}/common/ai.py +0 -0
  40. {screenforge-0.4.0 → screenforge-0.5.0}/common/ai_autonomous.py +0 -0
  41. {screenforge-0.4.0 → screenforge-0.5.0}/common/ai_heal.py +0 -0
  42. {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/__init__.py +0 -0
  43. {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_hash.py +0 -0
  44. {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_manager.py +0 -0
  45. {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_stats.py +0 -0
  46. {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_storage.py +0 -0
  47. {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/embedding_loader.py +0 -0
  48. {screenforge-0.4.0 → screenforge-0.5.0}/common/capabilities.py +0 -0
  49. {screenforge-0.4.0 → screenforge-0.5.0}/common/case_memory.py +0 -0
  50. {screenforge-0.4.0 → screenforge-0.5.0}/common/error_codes.py +0 -0
  51. {screenforge-0.4.0 → screenforge-0.5.0}/common/exceptions.py +0 -0
  52. {screenforge-0.4.0 → screenforge-0.5.0}/common/executor.py +0 -0
  53. {screenforge-0.4.0 → screenforge-0.5.0}/common/failure_diagnosis.py +0 -0
  54. {screenforge-0.4.0 → screenforge-0.5.0}/common/history_manager.py +0 -0
  55. {screenforge-0.4.0 → screenforge-0.5.0}/common/logs.py +0 -0
  56. {screenforge-0.4.0 → screenforge-0.5.0}/common/mcp_server.py +0 -0
  57. {screenforge-0.4.0 → screenforge-0.5.0}/common/preflight.py +0 -0
  58. {screenforge-0.4.0 → screenforge-0.5.0}/common/progress.py +0 -0
  59. {screenforge-0.4.0 → screenforge-0.5.0}/common/run_reporter.py +0 -0
  60. {screenforge-0.4.0 → screenforge-0.5.0}/common/run_resume.py +0 -0
  61. {screenforge-0.4.0 → screenforge-0.5.0}/common/runtime_modes.py +0 -0
  62. {screenforge-0.4.0 → screenforge-0.5.0}/common/tool_protocol.py +0 -0
  63. {screenforge-0.4.0 → screenforge-0.5.0}/common/visual_fallback.py +0 -0
  64. {screenforge-0.4.0 → screenforge-0.5.0}/common/workflow_schema.py +0 -0
  65. {screenforge-0.4.0 → screenforge-0.5.0}/config/__init__.py +0 -0
  66. {screenforge-0.4.0 → screenforge-0.5.0}/config/config.py +0 -0
  67. {screenforge-0.4.0 → screenforge-0.5.0}/config/env_loader.py +0 -0
  68. {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/dependency_links.txt +0 -0
  69. {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/entry_points.txt +0 -0
  70. {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/requires.txt +0 -0
  71. {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/top_level.txt +0 -0
  72. {screenforge-0.4.0 → screenforge-0.5.0}/setup.cfg +0 -0
  73. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ai_autonomous.py +0 -0
  74. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ai_brain.py +0 -0
  75. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ai_heal.py +0 -0
  76. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_android_smoke_live.py +0 -0
  77. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_cache_manager.py +0 -0
  78. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_capabilities.py +0 -0
  79. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_cli_action_json.py +0 -0
  80. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_codegen_quality.py +0 -0
  81. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_dispatch.py +0 -0
  82. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_doctor_orphan_browser.py +0 -0
  83. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_error_codes.py +0 -0
  84. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_executor.py +0 -0
  85. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_failure_diagnosis.py +0 -0
  86. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_interaction_actions.py +0 -0
  87. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ios_smoke_live.py +0 -0
  88. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_mcp_ref_cache.py +0 -0
  89. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ml_optional.py +0 -0
  90. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_run_reporter.py +0 -0
  91. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_run_resume.py +0 -0
  92. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_runtime_modes.py +0 -0
  93. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_screenshot_annotator.py +0 -0
  94. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_shorthand.py +0 -0
  95. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_tool_protocol_diagnosis.py +0 -0
  96. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_utils_ios.py +0 -0
  97. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_utils_web.py +0 -0
  98. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_utils_xml.py +0 -0
  99. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_visual_fallback.py +0 -0
  100. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_web_adapter.py +0 -0
  101. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_web_dom_complex_live.py +0 -0
  102. {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_web_smoke_live.py +0 -0
  103. {screenforge-0.4.0 → screenforge-0.5.0}/utils/__init__.py +0 -0
  104. {screenforge-0.4.0 → screenforge-0.5.0}/utils/screenshot_annotator.py +0 -0
  105. {screenforge-0.4.0 → screenforge-0.5.0}/utils/utils_ios.py +0 -0
  106. {screenforge-0.4.0 → screenforge-0.5.0}/utils/utils_web.py +0 -0
  107. {screenforge-0.4.0 → screenforge-0.5.0}/utils/utils_xml.py +0 -0
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: screenforge
3
+ Version: 0.5.0
4
+ Summary: AI-driven cross-platform UI automation engine with test script generation
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/jhinzzz/ScreenForge
7
+ Project-URL: Repository, https://github.com/jhinzzz/ScreenForge
8
+ Project-URL: Issues, https://github.com/jhinzzz/ScreenForge/issues
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: playwright>=1.50
13
+ Requires-Dist: openai>=2.0
14
+ Requires-Dist: allure-pytest>=2.15
15
+ Requires-Dist: loguru>=0.7
16
+ Requires-Dist: pydantic>=2.0
17
+ Requires-Dist: python-dotenv>=1.0
18
+ Requires-Dist: PyYAML>=6.0
19
+ Requires-Dist: pillow>=12.0
20
+ Requires-Dist: lxml>=5.0
21
+ Requires-Dist: requests>=2.30
22
+ Requires-Dist: rich>=14.0
23
+ Requires-Dist: typer>=0.24
24
+ Requires-Dist: retry2>=0.9
25
+ Requires-Dist: opencv-python>=4.10
26
+ Requires-Dist: numpy>=1.24
27
+ Requires-Dist: filelock>=3.12
28
+ Provides-Extra: android
29
+ Requires-Dist: uiautomator2>=3.5; extra == "android"
30
+ Requires-Dist: adbutils>=2.12; extra == "android"
31
+ Provides-Extra: ios
32
+ Requires-Dist: facebook-wda>=1.0; extra == "ios"
33
+ Provides-Extra: ml
34
+ Requires-Dist: torch>=2.0; extra == "ml"
35
+ Requires-Dist: transformers>=5.0; extra == "ml"
36
+ Requires-Dist: sentence-transformers>=5.0; extra == "ml"
37
+ Requires-Dist: scikit-learn>=1.8; extra == "ml"
38
+ Provides-Extra: playground
39
+ Requires-Dist: fastapi>=0.115; extra == "playground"
40
+ Requires-Dist: uvicorn>=0.34; extra == "playground"
41
+ Requires-Dist: websockets>=14.0; extra == "playground"
42
+ Provides-Extra: all
43
+ Requires-Dist: screenforge[android,ios,ml,playground]; extra == "all"
44
+ Dynamic: license-file
45
+
46
+ # ScreenForge
47
+
48
+ [![CI](https://github.com/jhinzzz/ScreenForge/actions/workflows/ci.yml/badge.svg)](https://github.com/jhinzzz/ScreenForge/actions/workflows/ci.yml)
49
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
51
+
52
+ **[中文](https://github.com/jhinzzz/ScreenForge/blob/main/README_CN.md)** | English
53
+
54
+ > Describe what to test. Watch it happen. Get a pytest script.
55
+
56
+ ScreenForge is an AI-driven UI automation engine that turns natural language into executable test scripts. Unlike record-and-replay tools, you don't perform the actions yourself — the AI does it for you.
57
+
58
+ ![ScreenForge Demo](https://raw.githubusercontent.com/jhinzzz/ScreenForge/main/docs/assets/demo.gif)
59
+
60
+ ## Why ScreenForge?
61
+
62
+ | | Playwright Codegen | Browser Use | Midscene.js | **ScreenForge** |
63
+ |---|---|---|---|---|
64
+ | Need to perform actions yourself? | Yes | No | No | **No** |
65
+ | Generates replayable test scripts? | Yes | No | No | **Yes (pytest)** |
66
+ | Self-healing when UI changes? | No | No | No | **Yes** |
67
+ | Works as AI Agent tool (MCP)? | No | Yes | No | **Yes** |
68
+
69
+ **Core architecture**: Your AI Agent is the brain (understands requirements, makes decisions). ScreenForge is the hands (executes UI actions, generates code).
70
+
71
+ ## Quick Start
72
+
73
+ ```bash
74
+ pip install screenforge
75
+
76
+ # See the magic without any API key:
77
+ screenforge --demo
78
+
79
+ # For real usage, set your LLM key:
80
+ export OPENAI_API_KEY=sk-...
81
+
82
+ # Inspect the current page (returns DOM tree for your Agent to analyze):
83
+ echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
84
+
85
+ # Execute a single action:
86
+ screenforge --action click --platform web --locator-type text --locator-value "Login"
87
+ ```
88
+
89
+ ## How It Works
90
+
91
+ ```
92
+ You (or your AI Agent) ScreenForge
93
+ │ │
94
+ ├──── "Test the login" ─────►│
95
+ │ ├── inspect_ui (get DOM tree)
96
+ │◄── DOM tree ──────────────┤
97
+ │ │
98
+ ├──── click #email ─────────►│
99
+ ├──── input "user@..." ─────►│
100
+ ├──── click "Sign In" ──────►│
101
+ │ │
102
+ │◄── pytest script ─────────┤
103
+ │◄── Allure report ─────────┤
104
+ ```
105
+
106
+ Each step: **inspect → decide → act → verify**. The AI decides, ScreenForge executes.
107
+
108
+ ## Features
109
+
110
+ - **Cross-platform**: Android (uiautomator2), iOS (wda), Web (Playwright)
111
+ - **Self-healing engine**: When tests break due to UI changes, the engine auto-repairs locators with confidence scoring and AST validation
112
+ - **L1/L2 semantic cache**: Same page + same instruction = instant response, no LLM call needed
113
+ - **Visual fallback**: When DOM can't locate elements (Canvas, games), VLM parses screenshots
114
+ - **MCP server**: Any MCP-compatible Agent can drive ScreenForge natively
115
+ - **Structured output**: JSON Lines events + `report/runs/<id>/` artifacts for CI integration
116
+ - **Live Mirror playground**: Watch the generated pytest code grow line-by-line beside a live screenshot as the test runs — `screenforge --playground`. See the [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md)
117
+
118
+ ## Agent Integration (Claude Code / Cursor / Codex)
119
+
120
+ ScreenForge exposes itself as a tool for AI Agents. The standard loop:
121
+
122
+ ```bash
123
+ # 1. Get page structure (your Agent analyzes it)
124
+ echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
125
+
126
+ # 2. Your Agent decides what to do, sends precise actions
127
+ screenforge --action click --platform web --locator-type text --locator-value "Login"
128
+
129
+ # 3. Verify the result, repeat
130
+ echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
131
+ ```
132
+
133
+ For batch operations, use workflows:
134
+
135
+ ```bash
136
+ screenforge --workflow ./workflows/login.yaml --platform web --json
137
+ ```
138
+
139
+ Or start the MCP server for native Agent integration:
140
+
141
+ ```bash
142
+ screenforge --mcp-server
143
+ ```
144
+
145
+ ## GitHub Actions
146
+
147
+ Add ScreenForge to your CI pipeline:
148
+
149
+ ```yaml
150
+ - uses: jhinzzz/ScreenForge@v1
151
+ with:
152
+ platform: web
153
+ workflow: ./workflows/login.yaml
154
+ openai-api-key: ${{ secrets.OPENAI_API_KEY }}
155
+ ```
156
+
157
+ Results are auto-uploaded as Allure artifacts. See [action.yml](https://github.com/jhinzzz/ScreenForge/blob/main/action.yml) for all inputs.
158
+
159
+ See [Agent Integration Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) for the complete protocol.
160
+
161
+ ## Installation (from source)
162
+
163
+ ```bash
164
+ git clone https://github.com/jhinzzz/ScreenForge.git
165
+ cd ScreenForge
166
+ python -m venv .venv && source .venv/bin/activate
167
+ pip install -e ".[all]"
168
+ ```
169
+
170
+ Platform-specific extras:
171
+ - Web only: `pip install -e .` (default, includes Playwright)
172
+ - Android: `pip install -e ".[android]"`
173
+ - iOS: `pip install -e ".[ios]"`
174
+ - ML/cache: `pip install -e ".[ml]"` (sentence-transformers for semantic cache)
175
+
176
+ ## Configuration
177
+
178
+ ```bash
179
+ # Required: LLM API key (OpenAI-compatible endpoint)
180
+ export OPENAI_API_KEY=sk-...
181
+
182
+ # Optional: custom endpoint (defaults to api.openai.com)
183
+ export OPENAI_BASE_URL=https://api.openai.com/v1
184
+
185
+ # Optional: model (defaults to gpt-4o)
186
+ export MODEL_NAME=gpt-4o
187
+ ```
188
+
189
+ Or create a `.env` file (copy from `.env_template`).
190
+
191
+ ## Badge
192
+
193
+ If ScreenForge generates tests for your project, add this badge to your README:
194
+
195
+ ```markdown
196
+ [![Tests by ScreenForge](https://img.shields.io/badge/tests%20by-ScreenForge-blue?logo=pytest)](https://github.com/jhinzzz/ScreenForge)
197
+ ```
198
+
199
+ [![Tests by ScreenForge](https://img.shields.io/badge/tests%20by-ScreenForge-blue?logo=pytest)](https://github.com/jhinzzz/ScreenForge)
200
+
201
+ ## Learn More
202
+
203
+ | Resource | Description |
204
+ |----------|-------------|
205
+ | [Mobile Setup](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mobile-setup.md) | Android & iOS device connection guide |
206
+ | [MCP Setup (3 min)](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mcp-setup.md) | Connect to Claude Desktop / Cursor / Cline / Claude Code |
207
+ | [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) | Integration protocol for AI Agents |
208
+ | [Capability Matrix](https://github.com/jhinzzz/ScreenForge/blob/main/docs/capability-matrix.md) | Supported platforms, actions, and locators |
209
+ | [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md) | Live Mirror — watch code + screenshots grow as the test runs |
210
+ | [Workflow Examples](https://github.com/jhinzzz/ScreenForge/tree/main/docs/workflows) | YAML workflow templates |
211
+ | [CHANGELOG](https://github.com/jhinzzz/ScreenForge/blob/main/CHANGELOG.md) | Version history |
212
+
213
+ ## Contributing
214
+
215
+ See [CONTRIBUTING.md](https://github.com/jhinzzz/ScreenForge/blob/main/CONTRIBUTING.md) for guidelines. Issues and PRs welcome!
216
+
217
+ ## License
218
+
219
+ [MIT](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
@@ -2,15 +2,15 @@
2
2
 
3
3
  [![CI](https://github.com/jhinzzz/ScreenForge/actions/workflows/ci.yml/badge.svg)](https://github.com/jhinzzz/ScreenForge/actions/workflows/ci.yml)
4
4
  [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
6
6
 
7
- **[中文](./README_CN.md)** | English
7
+ **[中文](https://github.com/jhinzzz/ScreenForge/blob/main/README_CN.md)** | English
8
8
 
9
9
  > Describe what to test. Watch it happen. Get a pytest script.
10
10
 
11
11
  ScreenForge is an AI-driven UI automation engine that turns natural language into executable test scripts. Unlike record-and-replay tools, you don't perform the actions yourself — the AI does it for you.
12
12
 
13
- ![ScreenForge Demo](docs/assets/demo.gif)
13
+ ![ScreenForge Demo](https://raw.githubusercontent.com/jhinzzz/ScreenForge/main/docs/assets/demo.gif)
14
14
 
15
15
  ## Why ScreenForge?
16
16
 
@@ -68,6 +68,7 @@ Each step: **inspect → decide → act → verify**. The AI decides, ScreenForg
68
68
  - **Visual fallback**: When DOM can't locate elements (Canvas, games), VLM parses screenshots
69
69
  - **MCP server**: Any MCP-compatible Agent can drive ScreenForge natively
70
70
  - **Structured output**: JSON Lines events + `report/runs/<id>/` artifacts for CI integration
71
+ - **Live Mirror playground**: Watch the generated pytest code grow line-by-line beside a live screenshot as the test runs — `screenforge --playground`. See the [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md)
71
72
 
72
73
  ## Agent Integration (Claude Code / Cursor / Codex)
73
74
 
@@ -108,9 +109,9 @@ Add ScreenForge to your CI pipeline:
108
109
  openai-api-key: ${{ secrets.OPENAI_API_KEY }}
109
110
  ```
110
111
 
111
- Results are auto-uploaded as Allure artifacts. See [action.yml](action.yml) for all inputs.
112
+ Results are auto-uploaded as Allure artifacts. See [action.yml](https://github.com/jhinzzz/ScreenForge/blob/main/action.yml) for all inputs.
112
113
 
113
- See [Agent Integration Guide](docs/agent_guide.md) for the complete protocol.
114
+ See [Agent Integration Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) for the complete protocol.
114
115
 
115
116
  ## Installation (from source)
116
117
 
@@ -156,17 +157,18 @@ If ScreenForge generates tests for your project, add this badge to your README:
156
157
 
157
158
  | Resource | Description |
158
159
  |----------|-------------|
159
- | [Mobile Setup](docs/mobile-setup.md) | Android & iOS device connection guide |
160
- | [MCP Setup (3 min)](docs/mcp-setup.md) | Connect to Claude Desktop / Cursor / Cline / Claude Code |
161
- | [Agent Guide](docs/agent_guide.md) | Integration protocol for AI Agents |
162
- | [Capability Matrix](docs/capability-matrix.md) | Supported platforms, actions, and locators |
163
- | [Workflow Examples](docs/workflows/) | YAML workflow templates |
164
- | [CHANGELOG](CHANGELOG.md) | Version history |
160
+ | [Mobile Setup](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mobile-setup.md) | Android & iOS device connection guide |
161
+ | [MCP Setup (3 min)](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mcp-setup.md) | Connect to Claude Desktop / Cursor / Cline / Claude Code |
162
+ | [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) | Integration protocol for AI Agents |
163
+ | [Capability Matrix](https://github.com/jhinzzz/ScreenForge/blob/main/docs/capability-matrix.md) | Supported platforms, actions, and locators |
164
+ | [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md) | Live Mirror watch code + screenshots grow as the test runs |
165
+ | [Workflow Examples](https://github.com/jhinzzz/ScreenForge/tree/main/docs/workflows) | YAML workflow templates |
166
+ | [CHANGELOG](https://github.com/jhinzzz/ScreenForge/blob/main/CHANGELOG.md) | Version history |
165
167
 
166
168
  ## Contributing
167
169
 
168
- See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Issues and PRs welcome!
170
+ See [CONTRIBUTING.md](https://github.com/jhinzzz/ScreenForge/blob/main/CONTRIBUTING.md) for guidelines. Issues and PRs welcome!
169
171
 
170
172
  ## License
171
173
 
172
- [MIT](LICENSE)
174
+ [MIT](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -5,6 +5,7 @@ import os
5
5
  import sys
6
6
 
7
7
  import cli.shared as _shared
8
+ from cli.playground_sink import build_sink_from_args, maybe_push_step
8
9
  from cli.reporter import (
9
10
  _apply_resume_summary,
10
11
  _build_action_summary,
@@ -206,6 +207,18 @@ def run_action_default_mode(
206
207
 
207
208
  history_manager.add_step(result["code_lines"], result["action_description"])
208
209
  save_to_disk(output_script_path, history_manager.get_current_file_content())
210
+ # ★ Live-mirror bypass (opt-in --playground-sink). join_on_exit=True: a bare
211
+ # --action exits right after, so wait briefly for the last frame to land.
212
+ maybe_push_step(
213
+ build_sink_from_args(args, join_on_exit=True),
214
+ args=args,
215
+ reporter=reporter,
216
+ adapter=adapter,
217
+ action_data=action_data,
218
+ result=result,
219
+ step_index=None, # resolver picks: session counter, or 1 for a bare action
220
+ file_path=output_script_path, # normalized to abs path inside build_step_event
221
+ )
209
222
  reporter.emit_event(
210
223
  "action_executed",
211
224
  step=1,
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
 
5
5
  import cli.shared as _shared
6
6
  from cli.modes.dry_run import _build_resolution_hint, _preview_action_resolution
7
+ from cli.playground_sink import build_sink_from_args, maybe_push_step
7
8
  from cli.reporter import (
8
9
  _apply_resume_summary,
9
10
  _build_reporter,
@@ -285,6 +286,9 @@ def run_workflow_default_mode(
285
286
  history_manager = _shared.StepHistoryManager(initial_content=header)
286
287
  save_to_disk(output_script_path, header)
287
288
  executor = _shared.UIExecutor(device, platform=args.platform)
289
+ # Workflow is one process with many steps (the main use case for live mirror).
290
+ # Build the sink once; join_on_exit=False — the process lives across all steps.
291
+ sink = build_sink_from_args(args, join_on_exit=False)
288
292
 
289
293
  executed_steps = 0
290
294
  for index, step in enumerate(workflow.steps, start=1):
@@ -315,6 +319,17 @@ def run_workflow_default_mode(
315
319
  result["code_lines"], result["action_description"]
316
320
  )
317
321
  save_to_disk(output_script_path, history_manager.get_current_file_content())
322
+ # ★ Live-mirror bypass: push this step with its loop counter as step_index.
323
+ maybe_push_step(
324
+ sink,
325
+ args=args,
326
+ reporter=reporter,
327
+ adapter=adapter,
328
+ action_data=action_data,
329
+ result=result,
330
+ step_index=index,
331
+ file_path=output_script_path, # normalized to abs path inside build_step_event
332
+ )
318
333
  reporter.emit_event(
319
334
  "action_executed",
320
335
  step=index,
@@ -151,6 +151,17 @@ def build_parser() -> argparse.ArgumentParser:
151
151
  default=7860,
152
152
  help="Playground server port (default: 7860)",
153
153
  )
154
+ parser.add_argument(
155
+ "--playground-sink",
156
+ action="store_true",
157
+ help="Push each step's code + screenshot to a running playground (opt-in; off = zero cost)",
158
+ )
159
+ parser.add_argument(
160
+ "--playground-url",
161
+ type=str,
162
+ default="http://127.0.0.1:7860",
163
+ help="Playground base URL for --playground-sink (default: http://127.0.0.1:7860)",
164
+ )
154
165
  parser.add_argument(
155
166
  "--device-url",
156
167
  type=str,
@@ -270,7 +281,10 @@ def validate_cli_args(args: argparse.Namespace) -> None:
270
281
  raise ValueError("--action cannot be combined with --goal or --workflow")
271
282
  has_demo = bool(getattr(args, "demo", False))
272
283
  has_init = bool(getattr(args, "init", False))
273
- if has_demo or has_init:
284
+ # --playground starts a standalone server (dispatch.py handles it after this
285
+ # validation, exactly like --init/--demo) — it needs no goal/workflow/action.
286
+ has_playground = bool(getattr(args, "playground", False))
287
+ if has_demo or has_init or has_playground:
274
288
  return
275
289
  has_session_end = bool(str(getattr(args, "session_end", "")).strip())
276
290
  if has_session_end:
@@ -0,0 +1,202 @@
1
+ """Fire-and-forget visualization sink: short-lived action process → resident playground.
2
+
3
+ Red line (G5): any network error is swallowed silently. A sink push MUST NEVER
4
+ slow down the action or change its exit code — the 0/1 exit code is a contract to
5
+ the agent (see CLAUDE.md). The sink is a bypass observer hung after save_to_disk;
6
+ it does not touch execute_and_record, codegen, or disk persistence.
7
+ """
8
+
9
+ import base64
10
+ import os
11
+ import threading
12
+
13
+ import requests # already in requirements.txt (requests==2.32.5) — zero new deps
14
+ from loguru import logger as log
15
+ from pydantic import BaseModel, Field
16
+
17
+ from cli.session import load_session
18
+
19
+ DEFAULT_PLAYGROUND_URL = "http://127.0.0.1:7860"
20
+
21
+ # Latency ceiling on the contract-protected single-step --action path (red line #1:
22
+ # "never slow the action down"). A reachable-but-slow playground must not tax the
23
+ # action beyond this documented budget. (connect, read) is split so a hung peer
24
+ # can't stall on connect; _JOIN_TIMEOUT is the hard cap the single-step process
25
+ # waits for the last frame to land before sys.exit — kept ≤ read+ε and well under
26
+ # human-perceptible. Worst added latency on --action ≈ _JOIN_TIMEOUT.
27
+ _POST_TIMEOUT = (0.2, 0.25) # (connect, read) seconds
28
+ _JOIN_TIMEOUT = 0.3 # seconds; single-step last-frame grace
29
+
30
+
31
+ class PlaygroundStepEvent(BaseModel):
32
+ """One step pushed to the playground. Shape == the frontend SSE `step` contract."""
33
+
34
+ run_id: str
35
+ step_index: int # ⭐ time-travel seed: data accumulates/replays by this index
36
+ code_lines: list[str] = Field(default_factory=list)
37
+ action_description: str = ""
38
+ action: str = ""
39
+ locator_type: str = ""
40
+ locator_value: str = ""
41
+ extra_value: str = ""
42
+ success: bool = True
43
+ screenshot_b64: str = "" # empty = no screenshot this step (degrade, never crash)
44
+ file_path: str = "" # abs path of the generated test file (for "open in IDE")
45
+
46
+
47
+ class PlaygroundSink:
48
+ """Pushes each step to a running playground, best-effort.
49
+
50
+ Disabled by default: enabled=False means zero cost — no HTTP, no thread, and
51
+ (at the call sites) take_screenshot is never even invoked.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ base_url: str = DEFAULT_PLAYGROUND_URL,
57
+ enabled: bool = False,
58
+ join_on_exit: bool = False,
59
+ ):
60
+ self.base_url = base_url.rstrip("/")
61
+ self.enabled = enabled
62
+ # Single-step --action exits the process right after push_step returns;
63
+ # join a short window so the daemon thread's last frame can land (§6 单步收尾).
64
+ self._join_on_exit = join_on_exit
65
+
66
+ def push_step(self, event: PlaygroundStepEvent) -> None:
67
+ """Best-effort push. Hands off to a daemon thread; the caller returns at
68
+ once (arch#3: never block the action hot path)."""
69
+ if not self.enabled:
70
+ return
71
+ t = threading.Thread(target=self._post, args=(event,), daemon=True)
72
+ t.start()
73
+ if self._join_on_exit:
74
+ # Single-step --action exits right after this returns; wait a bounded
75
+ # grace (≤ _JOIN_TIMEOUT) for the last frame to land. This is the hard
76
+ # ceiling on added latency to the contract path — never grow it past a
77
+ # human-imperceptible budget (HIGH-1 from review: 0.6s was too generous).
78
+ t.join(timeout=_JOIN_TIMEOUT)
79
+
80
+ def _post(self, event: PlaygroundStepEvent) -> None:
81
+ try:
82
+ requests.post(
83
+ f"{self.base_url}/api/step",
84
+ json=event.model_dump(),
85
+ timeout=_POST_TIMEOUT, # (connect, read) split: a hung playground can't stall us
86
+ )
87
+ except Exception as e: # ConnectionError / Timeout / anything — swallow (G5)
88
+ log.debug(f"[playground-sink] skip (playground unreachable): {e}")
89
+
90
+ @staticmethod
91
+ def encode_screenshot(adapter) -> str:
92
+ """Cross-platform: take_screenshot() -> bytes → base64. Can't grab → '' (degrade).
93
+
94
+ Platform-agnostic on purpose: all three adapters expose take_screenshot()
95
+ (base_adapter.py:17 abstract), so no per-platform branching is needed here.
96
+ """
97
+ try:
98
+ png = adapter.take_screenshot()
99
+ return base64.b64encode(png).decode() if png else ""
100
+ except Exception as e:
101
+ log.debug(f"[playground-sink] screenshot skip: {e}")
102
+ return ""
103
+
104
+
105
+ def build_step_event(
106
+ *,
107
+ run_key: str,
108
+ step_index: int,
109
+ action_data: dict,
110
+ result: dict,
111
+ screenshot_b64: str,
112
+ file_path: str = "",
113
+ ) -> PlaygroundStepEvent:
114
+ """MANDATORY single construction point for every step event (code#4).
115
+
116
+ All three entry points (action / workflow / main) build events ONLY through
117
+ here. Adding a field later (e.g. a seed timestamp) is then one edit, not three
118
+ — preventing the P9-style schema split where one call site silently drifts.
119
+
120
+ file_path is normalized to an absolute path HERE (one idiom, one place) rather
121
+ than at each call site, so it happens inside maybe_push_step's G5 try/except.
122
+ Empty stays empty — abspath('') would wrongly yield the cwd, and the frontend
123
+ treats '' as "no openable file" (disables the IDE button), so guard it.
124
+ """
125
+ return PlaygroundStepEvent(
126
+ run_id=run_key,
127
+ step_index=step_index,
128
+ code_lines=result.get("code_lines", []) or [],
129
+ action_description=result.get("action_description", ""),
130
+ action=action_data.get("action", ""),
131
+ locator_type=action_data.get("locator_type", ""),
132
+ locator_value=action_data.get("locator_value", ""),
133
+ extra_value=action_data.get("extra_value", ""),
134
+ success=bool(result.get("success", True)),
135
+ screenshot_b64=screenshot_b64,
136
+ file_path=os.path.abspath(file_path) if file_path else "",
137
+ )
138
+
139
+
140
+ def build_sink_from_args(args, *, join_on_exit: bool = False) -> "PlaygroundSink":
141
+ """Construct a sink from parsed CLI args. Absent flags → disabled (zero cost)."""
142
+ return PlaygroundSink(
143
+ base_url=getattr(args, "playground_url", "") or DEFAULT_PLAYGROUND_URL,
144
+ enabled=bool(getattr(args, "playground_sink", False)),
145
+ join_on_exit=join_on_exit,
146
+ )
147
+
148
+
149
+ def maybe_push_step(
150
+ sink: "PlaygroundSink",
151
+ *,
152
+ args,
153
+ reporter,
154
+ adapter,
155
+ action_data: dict,
156
+ result: dict,
157
+ step_index: int | None = None,
158
+ file_path: str = "",
159
+ ) -> None:
160
+ """The ONE guarded entry point every call site uses (action / workflow / main).
161
+
162
+ Disabled-fast: returns before touching the adapter, so take_screenshot is
163
+ never called and there is zero device I/O or network on the hot path when the
164
+ sink is off. Wrapped in a blanket try/except as a belt-and-suspenders G5
165
+ guard — the bypass observer must never break the action it observes.
166
+ """
167
+ if not sink.enabled:
168
+ return
169
+ try:
170
+ run_key, resolved_index = resolve_playground_run_key(args, reporter)
171
+ event = build_step_event(
172
+ run_key=run_key,
173
+ step_index=step_index if step_index is not None else resolved_index,
174
+ action_data=action_data,
175
+ result=result,
176
+ screenshot_b64=PlaygroundSink.encode_screenshot(adapter),
177
+ file_path=file_path,
178
+ )
179
+ sink.push_step(event)
180
+ except Exception as e: # never let visualization break the observed action
181
+ log.debug(f"[playground-sink] push skipped: {e}")
182
+
183
+
184
+ def resolve_playground_run_key(args, reporter) -> tuple[str, int]:
185
+ """Return (run_key, step_index) — the cross-process-stable playground timeline key.
186
+
187
+ Root cause (arch#1): run_reporter.py mints run_id as `timestamp_uuid`, unique
188
+ per short-lived process. In agent mode each --action is its own process, so
189
+ using reporter.run_id directly would shatter a 5-step flow into 5 single-step
190
+ buckets and the seed's timeline would be born broken.
191
+
192
+ --session-id present → use session_id as the key (one session = one timeline);
193
+ step_index comes from the session's persisted 'steps' counter (cli/session.py),
194
+ which dispatch.py increments AFTER each successful step, so steps+1 is the
195
+ 1-based index of the step about to be pushed.
196
+ No session → a bare --action is inherently single-step: reporter.run_id, index 1.
197
+ """
198
+ session_id = getattr(args, "session_id", "") or getattr(args, "session_end", "")
199
+ if session_id:
200
+ session = load_session(session_id)
201
+ return session_id, (session.get("steps", 0) + 1 if session else 1)
202
+ return reporter.run_id, 1
@@ -1,7 +1,8 @@
1
1
  [project]
2
2
  name = "screenforge"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "AI-driven cross-platform UI automation engine with test script generation"
5
+ readme = "README.md"
5
6
  requires-python = ">=3.11"
6
7
  license = {text = "MIT"}
7
8