screenforge 0.4.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.
- screenforge-0.4.0/LICENSE +21 -0
- screenforge-0.4.0/PKG-INFO +43 -0
- screenforge-0.4.0/README.md +172 -0
- screenforge-0.4.0/cli/__init__.py +0 -0
- screenforge-0.4.0/cli/_version.py +1 -0
- screenforge-0.4.0/cli/dispatch.py +266 -0
- screenforge-0.4.0/cli/doctor.py +487 -0
- screenforge-0.4.0/cli/modes/__init__.py +0 -0
- screenforge-0.4.0/cli/modes/action.py +262 -0
- screenforge-0.4.0/cli/modes/default.py +248 -0
- screenforge-0.4.0/cli/modes/demo.py +162 -0
- screenforge-0.4.0/cli/modes/dry_run.py +237 -0
- screenforge-0.4.0/cli/modes/init.py +133 -0
- screenforge-0.4.0/cli/modes/plan.py +148 -0
- screenforge-0.4.0/cli/modes/workflow.py +354 -0
- screenforge-0.4.0/cli/parser.py +305 -0
- screenforge-0.4.0/cli/reporter.py +207 -0
- screenforge-0.4.0/cli/session.py +146 -0
- screenforge-0.4.0/cli/shared.py +427 -0
- screenforge-0.4.0/cli/shorthand.py +90 -0
- screenforge-0.4.0/cli/tool_protocol_handlers.py +446 -0
- screenforge-0.4.0/common/__init__.py +0 -0
- screenforge-0.4.0/common/adapters/__init__.py +21 -0
- screenforge-0.4.0/common/adapters/android_adapter.py +273 -0
- screenforge-0.4.0/common/adapters/base_adapter.py +24 -0
- screenforge-0.4.0/common/adapters/ios_adapter.py +278 -0
- screenforge-0.4.0/common/adapters/web_adapter.py +271 -0
- screenforge-0.4.0/common/ai.py +277 -0
- screenforge-0.4.0/common/ai_autonomous.py +273 -0
- screenforge-0.4.0/common/ai_heal.py +222 -0
- screenforge-0.4.0/common/cache/__init__.py +15 -0
- screenforge-0.4.0/common/cache/cache_hash.py +57 -0
- screenforge-0.4.0/common/cache/cache_manager.py +300 -0
- screenforge-0.4.0/common/cache/cache_stats.py +133 -0
- screenforge-0.4.0/common/cache/cache_storage.py +79 -0
- screenforge-0.4.0/common/cache/embedding_loader.py +150 -0
- screenforge-0.4.0/common/capabilities.py +121 -0
- screenforge-0.4.0/common/case_memory.py +327 -0
- screenforge-0.4.0/common/error_codes.py +61 -0
- screenforge-0.4.0/common/exceptions.py +18 -0
- screenforge-0.4.0/common/executor.py +1504 -0
- screenforge-0.4.0/common/failure_diagnosis.py +138 -0
- screenforge-0.4.0/common/history_manager.py +75 -0
- screenforge-0.4.0/common/logs.py +168 -0
- screenforge-0.4.0/common/mcp_server.py +467 -0
- screenforge-0.4.0/common/preflight.py +496 -0
- screenforge-0.4.0/common/progress.py +37 -0
- screenforge-0.4.0/common/run_reporter.py +415 -0
- screenforge-0.4.0/common/run_resume.py +149 -0
- screenforge-0.4.0/common/runtime_modes.py +35 -0
- screenforge-0.4.0/common/tool_protocol.py +196 -0
- screenforge-0.4.0/common/visual_fallback.py +71 -0
- screenforge-0.4.0/common/workflow_schema.py +150 -0
- screenforge-0.4.0/config/__init__.py +0 -0
- screenforge-0.4.0/config/config.py +167 -0
- screenforge-0.4.0/config/env_loader.py +76 -0
- screenforge-0.4.0/pyproject.toml +107 -0
- screenforge-0.4.0/screenforge.egg-info/PKG-INFO +43 -0
- screenforge-0.4.0/screenforge.egg-info/SOURCES.txt +98 -0
- screenforge-0.4.0/screenforge.egg-info/dependency_links.txt +1 -0
- screenforge-0.4.0/screenforge.egg-info/entry_points.txt +2 -0
- screenforge-0.4.0/screenforge.egg-info/requires.txt +37 -0
- screenforge-0.4.0/screenforge.egg-info/top_level.txt +4 -0
- screenforge-0.4.0/setup.cfg +4 -0
- screenforge-0.4.0/tests/test_ai_autonomous.py +139 -0
- screenforge-0.4.0/tests/test_ai_brain.py +106 -0
- screenforge-0.4.0/tests/test_ai_heal.py +45 -0
- screenforge-0.4.0/tests/test_android_smoke_live.py +379 -0
- screenforge-0.4.0/tests/test_cache_manager.py +94 -0
- screenforge-0.4.0/tests/test_capabilities.py +98 -0
- screenforge-0.4.0/tests/test_cli_action_json.py +51 -0
- screenforge-0.4.0/tests/test_codegen_quality.py +333 -0
- screenforge-0.4.0/tests/test_dispatch.py +96 -0
- screenforge-0.4.0/tests/test_doctor_orphan_browser.py +147 -0
- screenforge-0.4.0/tests/test_error_codes.py +31 -0
- screenforge-0.4.0/tests/test_executor.py +604 -0
- screenforge-0.4.0/tests/test_failure_diagnosis.py +98 -0
- screenforge-0.4.0/tests/test_interaction_actions.py +230 -0
- screenforge-0.4.0/tests/test_ios_smoke_live.py +353 -0
- screenforge-0.4.0/tests/test_mcp_ref_cache.py +90 -0
- screenforge-0.4.0/tests/test_ml_optional.py +72 -0
- screenforge-0.4.0/tests/test_parser.py +146 -0
- screenforge-0.4.0/tests/test_run_reporter.py +220 -0
- screenforge-0.4.0/tests/test_run_resume.py +165 -0
- screenforge-0.4.0/tests/test_runtime_modes.py +56 -0
- screenforge-0.4.0/tests/test_screenshot_annotator.py +47 -0
- screenforge-0.4.0/tests/test_shorthand.py +124 -0
- screenforge-0.4.0/tests/test_tool_protocol_diagnosis.py +29 -0
- screenforge-0.4.0/tests/test_utils_ios.py +362 -0
- screenforge-0.4.0/tests/test_utils_web.py +38 -0
- screenforge-0.4.0/tests/test_utils_xml.py +365 -0
- screenforge-0.4.0/tests/test_visual_fallback.py +43 -0
- screenforge-0.4.0/tests/test_web_adapter.py +128 -0
- screenforge-0.4.0/tests/test_web_dom_complex_live.py +737 -0
- screenforge-0.4.0/tests/test_web_smoke_live.py +547 -0
- screenforge-0.4.0/utils/__init__.py +0 -0
- screenforge-0.4.0/utils/screenshot_annotator.py +60 -0
- screenforge-0.4.0/utils/utils_ios.py +195 -0
- screenforge-0.4.0/utils/utils_web.py +304 -0
- screenforge-0.4.0/utils/utils_xml.py +218 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jhinzzz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: screenforge
|
|
3
|
+
Version: 0.4.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
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: playwright>=1.50
|
|
12
|
+
Requires-Dist: openai>=2.0
|
|
13
|
+
Requires-Dist: allure-pytest>=2.15
|
|
14
|
+
Requires-Dist: loguru>=0.7
|
|
15
|
+
Requires-Dist: pydantic>=2.0
|
|
16
|
+
Requires-Dist: python-dotenv>=1.0
|
|
17
|
+
Requires-Dist: PyYAML>=6.0
|
|
18
|
+
Requires-Dist: pillow>=12.0
|
|
19
|
+
Requires-Dist: lxml>=5.0
|
|
20
|
+
Requires-Dist: requests>=2.30
|
|
21
|
+
Requires-Dist: rich>=14.0
|
|
22
|
+
Requires-Dist: typer>=0.24
|
|
23
|
+
Requires-Dist: retry2>=0.9
|
|
24
|
+
Requires-Dist: opencv-python>=4.10
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Requires-Dist: filelock>=3.12
|
|
27
|
+
Provides-Extra: android
|
|
28
|
+
Requires-Dist: uiautomator2>=3.5; extra == "android"
|
|
29
|
+
Requires-Dist: adbutils>=2.12; extra == "android"
|
|
30
|
+
Provides-Extra: ios
|
|
31
|
+
Requires-Dist: facebook-wda>=1.0; extra == "ios"
|
|
32
|
+
Provides-Extra: ml
|
|
33
|
+
Requires-Dist: torch>=2.0; extra == "ml"
|
|
34
|
+
Requires-Dist: transformers>=5.0; extra == "ml"
|
|
35
|
+
Requires-Dist: sentence-transformers>=5.0; extra == "ml"
|
|
36
|
+
Requires-Dist: scikit-learn>=1.8; extra == "ml"
|
|
37
|
+
Provides-Extra: playground
|
|
38
|
+
Requires-Dist: fastapi>=0.115; extra == "playground"
|
|
39
|
+
Requires-Dist: uvicorn>=0.34; extra == "playground"
|
|
40
|
+
Requires-Dist: websockets>=14.0; extra == "playground"
|
|
41
|
+
Provides-Extra: all
|
|
42
|
+
Requires-Dist: screenforge[android,ios,ml,playground]; extra == "all"
|
|
43
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# ScreenForge
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jhinzzz/ScreenForge/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**[中文](./README_CN.md)** | English
|
|
8
|
+
|
|
9
|
+
> Describe what to test. Watch it happen. Get a pytest script.
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Why ScreenForge?
|
|
16
|
+
|
|
17
|
+
| | Playwright Codegen | Browser Use | Midscene.js | **ScreenForge** |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| Need to perform actions yourself? | Yes | No | No | **No** |
|
|
20
|
+
| Generates replayable test scripts? | Yes | No | No | **Yes (pytest)** |
|
|
21
|
+
| Self-healing when UI changes? | No | No | No | **Yes** |
|
|
22
|
+
| Works as AI Agent tool (MCP)? | No | Yes | No | **Yes** |
|
|
23
|
+
|
|
24
|
+
**Core architecture**: Your AI Agent is the brain (understands requirements, makes decisions). ScreenForge is the hands (executes UI actions, generates code).
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install screenforge
|
|
30
|
+
|
|
31
|
+
# See the magic without any API key:
|
|
32
|
+
screenforge --demo
|
|
33
|
+
|
|
34
|
+
# For real usage, set your LLM key:
|
|
35
|
+
export OPENAI_API_KEY=sk-...
|
|
36
|
+
|
|
37
|
+
# Inspect the current page (returns DOM tree for your Agent to analyze):
|
|
38
|
+
echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
|
|
39
|
+
|
|
40
|
+
# Execute a single action:
|
|
41
|
+
screenforge --action click --platform web --locator-type text --locator-value "Login"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How It Works
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
You (or your AI Agent) ScreenForge
|
|
48
|
+
│ │
|
|
49
|
+
├──── "Test the login" ─────►│
|
|
50
|
+
│ ├── inspect_ui (get DOM tree)
|
|
51
|
+
│◄── DOM tree ──────────────┤
|
|
52
|
+
│ │
|
|
53
|
+
├──── click #email ─────────►│
|
|
54
|
+
├──── input "user@..." ─────►│
|
|
55
|
+
├──── click "Sign In" ──────►│
|
|
56
|
+
│ │
|
|
57
|
+
│◄── pytest script ─────────┤
|
|
58
|
+
│◄── Allure report ─────────┤
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each step: **inspect → decide → act → verify**. The AI decides, ScreenForge executes.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **Cross-platform**: Android (uiautomator2), iOS (wda), Web (Playwright)
|
|
66
|
+
- **Self-healing engine**: When tests break due to UI changes, the engine auto-repairs locators with confidence scoring and AST validation
|
|
67
|
+
- **L1/L2 semantic cache**: Same page + same instruction = instant response, no LLM call needed
|
|
68
|
+
- **Visual fallback**: When DOM can't locate elements (Canvas, games), VLM parses screenshots
|
|
69
|
+
- **MCP server**: Any MCP-compatible Agent can drive ScreenForge natively
|
|
70
|
+
- **Structured output**: JSON Lines events + `report/runs/<id>/` artifacts for CI integration
|
|
71
|
+
|
|
72
|
+
## Agent Integration (Claude Code / Cursor / Codex)
|
|
73
|
+
|
|
74
|
+
ScreenForge exposes itself as a tool for AI Agents. The standard loop:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 1. Get page structure (your Agent analyzes it)
|
|
78
|
+
echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
|
|
79
|
+
|
|
80
|
+
# 2. Your Agent decides what to do, sends precise actions
|
|
81
|
+
screenforge --action click --platform web --locator-type text --locator-value "Login"
|
|
82
|
+
|
|
83
|
+
# 3. Verify the result, repeat
|
|
84
|
+
echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For batch operations, use workflows:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
screenforge --workflow ./workflows/login.yaml --platform web --json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or start the MCP server for native Agent integration:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
screenforge --mcp-server
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## GitHub Actions
|
|
100
|
+
|
|
101
|
+
Add ScreenForge to your CI pipeline:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
- uses: jhinzzz/ScreenForge@v1
|
|
105
|
+
with:
|
|
106
|
+
platform: web
|
|
107
|
+
workflow: ./workflows/login.yaml
|
|
108
|
+
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Results are auto-uploaded as Allure artifacts. See [action.yml](action.yml) for all inputs.
|
|
112
|
+
|
|
113
|
+
See [Agent Integration Guide](docs/agent_guide.md) for the complete protocol.
|
|
114
|
+
|
|
115
|
+
## Installation (from source)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git clone https://github.com/jhinzzz/ScreenForge.git
|
|
119
|
+
cd ScreenForge
|
|
120
|
+
python -m venv .venv && source .venv/bin/activate
|
|
121
|
+
pip install -e ".[all]"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Platform-specific extras:
|
|
125
|
+
- Web only: `pip install -e .` (default, includes Playwright)
|
|
126
|
+
- Android: `pip install -e ".[android]"`
|
|
127
|
+
- iOS: `pip install -e ".[ios]"`
|
|
128
|
+
- ML/cache: `pip install -e ".[ml]"` (sentence-transformers for semantic cache)
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Required: LLM API key (OpenAI-compatible endpoint)
|
|
134
|
+
export OPENAI_API_KEY=sk-...
|
|
135
|
+
|
|
136
|
+
# Optional: custom endpoint (defaults to api.openai.com)
|
|
137
|
+
export OPENAI_BASE_URL=https://api.openai.com/v1
|
|
138
|
+
|
|
139
|
+
# Optional: model (defaults to gpt-4o)
|
|
140
|
+
export MODEL_NAME=gpt-4o
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Or create a `.env` file (copy from `.env_template`).
|
|
144
|
+
|
|
145
|
+
## Badge
|
|
146
|
+
|
|
147
|
+
If ScreenForge generates tests for your project, add this badge to your README:
|
|
148
|
+
|
|
149
|
+
```markdown
|
|
150
|
+
[](https://github.com/jhinzzz/ScreenForge)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
[](https://github.com/jhinzzz/ScreenForge)
|
|
154
|
+
|
|
155
|
+
## Learn More
|
|
156
|
+
|
|
157
|
+
| Resource | Description |
|
|
158
|
+
|----------|-------------|
|
|
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 |
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Issues and PRs welcome!
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
[MIT](LICENSE)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Execution dispatcher and CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from cli.doctor import run_capabilities_mode, run_doctor_mode
|
|
6
|
+
from cli.modes.action import run_action_default_mode
|
|
7
|
+
from cli.modes.default import run_default_mode
|
|
8
|
+
from cli.modes.dry_run import run_action_dry_run_mode, run_dry_run_mode
|
|
9
|
+
from cli.modes.plan import run_action_plan_only_mode, run_plan_only_mode
|
|
10
|
+
from cli.modes.workflow import (
|
|
11
|
+
run_workflow_default_mode,
|
|
12
|
+
run_workflow_dry_run_mode,
|
|
13
|
+
run_workflow_plan_only_mode,
|
|
14
|
+
)
|
|
15
|
+
from cli.parser import build_parser, validate_cli_args
|
|
16
|
+
from cli.reporter import _load_context_content, _resolve_output_script_path
|
|
17
|
+
from cli.shared import _SharedAdapterManager, config, log
|
|
18
|
+
from cli.tool_protocol_handlers import (
|
|
19
|
+
_requires_model_runtime,
|
|
20
|
+
run_mcp_server_mode,
|
|
21
|
+
run_tool_request_mode,
|
|
22
|
+
run_tool_stdin_mode,
|
|
23
|
+
)
|
|
24
|
+
from common.run_resume import RunContextLoadError
|
|
25
|
+
from common.runtime_modes import (
|
|
26
|
+
MODE_DOCTOR,
|
|
27
|
+
MODE_DRY_RUN,
|
|
28
|
+
MODE_PLAN_ONLY,
|
|
29
|
+
resolve_execution_mode,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _dispatch_execution(
|
|
34
|
+
args,
|
|
35
|
+
execution_mode: str,
|
|
36
|
+
output_script_path: str,
|
|
37
|
+
context_content: str,
|
|
38
|
+
resume_context: dict,
|
|
39
|
+
shared_adapter_manager: _SharedAdapterManager | None = None,
|
|
40
|
+
) -> int:
|
|
41
|
+
if execution_mode == MODE_DOCTOR:
|
|
42
|
+
return run_doctor_mode(args, output_script_path)
|
|
43
|
+
if args.workflow and execution_mode == MODE_PLAN_ONLY:
|
|
44
|
+
return run_workflow_plan_only_mode(
|
|
45
|
+
args,
|
|
46
|
+
output_script_path,
|
|
47
|
+
resume_context,
|
|
48
|
+
)
|
|
49
|
+
if args.workflow and execution_mode == MODE_DRY_RUN:
|
|
50
|
+
return run_workflow_dry_run_mode(
|
|
51
|
+
args,
|
|
52
|
+
output_script_path,
|
|
53
|
+
resume_context,
|
|
54
|
+
)
|
|
55
|
+
if args.workflow:
|
|
56
|
+
return run_workflow_default_mode(
|
|
57
|
+
args,
|
|
58
|
+
output_script_path,
|
|
59
|
+
resume_context,
|
|
60
|
+
)
|
|
61
|
+
if args.action and execution_mode == MODE_PLAN_ONLY:
|
|
62
|
+
return run_action_plan_only_mode(
|
|
63
|
+
args,
|
|
64
|
+
output_script_path,
|
|
65
|
+
resume_context,
|
|
66
|
+
)
|
|
67
|
+
if args.action and execution_mode == MODE_DRY_RUN:
|
|
68
|
+
return run_action_dry_run_mode(
|
|
69
|
+
args,
|
|
70
|
+
output_script_path,
|
|
71
|
+
resume_context,
|
|
72
|
+
)
|
|
73
|
+
if args.action:
|
|
74
|
+
return run_action_default_mode(
|
|
75
|
+
args,
|
|
76
|
+
output_script_path,
|
|
77
|
+
resume_context,
|
|
78
|
+
shared_adapter_manager=shared_adapter_manager,
|
|
79
|
+
)
|
|
80
|
+
if execution_mode == MODE_PLAN_ONLY:
|
|
81
|
+
return run_plan_only_mode(
|
|
82
|
+
args,
|
|
83
|
+
output_script_path,
|
|
84
|
+
context_content,
|
|
85
|
+
resume_context,
|
|
86
|
+
)
|
|
87
|
+
if execution_mode == MODE_DRY_RUN:
|
|
88
|
+
return run_dry_run_mode(
|
|
89
|
+
args,
|
|
90
|
+
output_script_path,
|
|
91
|
+
context_content,
|
|
92
|
+
resume_context,
|
|
93
|
+
)
|
|
94
|
+
return run_default_mode(
|
|
95
|
+
args,
|
|
96
|
+
output_script_path,
|
|
97
|
+
context_content,
|
|
98
|
+
resume_context,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _run_session_end(args) -> int:
|
|
103
|
+
from cli.session import delete_session, load_session, stop_session_recording
|
|
104
|
+
|
|
105
|
+
session_id = args.session_end
|
|
106
|
+
session = load_session(session_id)
|
|
107
|
+
if not session:
|
|
108
|
+
log.error(f"❌ Session not found: {session_id}")
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
output_path = session["output_path"]
|
|
112
|
+
|
|
113
|
+
video_path = stop_session_recording(session_id)
|
|
114
|
+
if video_path:
|
|
115
|
+
log.info(f"🎬 [Session] Recording saved: {video_path}")
|
|
116
|
+
|
|
117
|
+
delete_session(session_id)
|
|
118
|
+
log.info(f"✅ [Session] Ended session '{session_id}'")
|
|
119
|
+
log.info(f"📄 [Session] Test script: {output_path}")
|
|
120
|
+
log.info(f"📊 [Session] Total steps: {session.get('steps', 0)}")
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main():
|
|
125
|
+
from cli.shorthand import preprocess_argv
|
|
126
|
+
|
|
127
|
+
processed_argv = preprocess_argv(sys.argv)
|
|
128
|
+
|
|
129
|
+
parser = build_parser()
|
|
130
|
+
args = parser.parse_args(processed_argv[1:])
|
|
131
|
+
|
|
132
|
+
if getattr(args, "session_end", ""):
|
|
133
|
+
sys.exit(_run_session_end(args))
|
|
134
|
+
|
|
135
|
+
if getattr(args, "web_stop", False):
|
|
136
|
+
from common.adapters.web_adapter import stop_persistent_browser
|
|
137
|
+
|
|
138
|
+
# Idempotent: stopping a browser, or finding none to stop, both succeed.
|
|
139
|
+
stop_persistent_browser()
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
validate_cli_args(args)
|
|
144
|
+
except ValueError as e:
|
|
145
|
+
log.error(f"[E010] Invalid CLI arguments: {e}. Fix: run 'screenforge --help' to see valid options")
|
|
146
|
+
sys.exit(2)
|
|
147
|
+
|
|
148
|
+
if getattr(args, "init", False):
|
|
149
|
+
from cli.modes.init import run_init_mode
|
|
150
|
+
|
|
151
|
+
sys.exit(run_init_mode())
|
|
152
|
+
|
|
153
|
+
if getattr(args, "demo", False):
|
|
154
|
+
from cli.modes.demo import run_demo_mode
|
|
155
|
+
|
|
156
|
+
sys.exit(run_demo_mode())
|
|
157
|
+
|
|
158
|
+
if getattr(args, "playground", False):
|
|
159
|
+
try:
|
|
160
|
+
from playground.app import run_server
|
|
161
|
+
except ImportError:
|
|
162
|
+
log.error("[E013] Playground requires extra dependencies. Fix: pip install screenforge[playground]")
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
cdp_url = "http://127.0.0.1:9333"
|
|
165
|
+
log.info(f"Starting Playground on http://127.0.0.1:{args.playground_port}")
|
|
166
|
+
log.info(f"CDP screencast target: {cdp_url}")
|
|
167
|
+
run_server(port=args.playground_port, cdp_url=cdp_url)
|
|
168
|
+
sys.exit(0)
|
|
169
|
+
|
|
170
|
+
if args.tool_stdin:
|
|
171
|
+
from common.progress import set_tool_mode
|
|
172
|
+
set_tool_mode(True)
|
|
173
|
+
if sys.stdin.isatty():
|
|
174
|
+
import io
|
|
175
|
+
import json as _json
|
|
176
|
+
sys.stdin = io.StringIO(_json.dumps({"operation": "inspect_ui", "platform": args.platform}))
|
|
177
|
+
sys.exit(run_tool_stdin_mode(args))
|
|
178
|
+
|
|
179
|
+
if args.mcp_server:
|
|
180
|
+
from common.progress import set_tool_mode
|
|
181
|
+
set_tool_mode(True)
|
|
182
|
+
sys.exit(run_mcp_server_mode(args))
|
|
183
|
+
|
|
184
|
+
if args.tool_request:
|
|
185
|
+
from common.progress import set_tool_mode
|
|
186
|
+
set_tool_mode(True)
|
|
187
|
+
sys.exit(run_tool_request_mode(args))
|
|
188
|
+
|
|
189
|
+
if args.capabilities:
|
|
190
|
+
sys.exit(run_capabilities_mode(args))
|
|
191
|
+
|
|
192
|
+
execution_mode = resolve_execution_mode(
|
|
193
|
+
doctor=args.doctor,
|
|
194
|
+
plan_only=args.plan_only,
|
|
195
|
+
dry_run=args.dry_run,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
session_id = getattr(args, "session_id", "")
|
|
199
|
+
shared_adapter_mgr = None
|
|
200
|
+
if session_id and args.action:
|
|
201
|
+
from cli.session import (
|
|
202
|
+
create_session,
|
|
203
|
+
load_session,
|
|
204
|
+
resolve_session_output_path,
|
|
205
|
+
update_session,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
session = load_session(session_id)
|
|
209
|
+
if session:
|
|
210
|
+
output_script_path = session["output_path"]
|
|
211
|
+
shared_adapter_mgr = _SharedAdapterManager()
|
|
212
|
+
else:
|
|
213
|
+
from cli.session import start_session_recording
|
|
214
|
+
|
|
215
|
+
output_script_path = resolve_session_output_path(session_id, args.platform)
|
|
216
|
+
session = create_session(session_id, args.platform, output_script_path)
|
|
217
|
+
shared_adapter_mgr = _SharedAdapterManager()
|
|
218
|
+
start_session_recording(session_id, args.platform)
|
|
219
|
+
else:
|
|
220
|
+
output_script_path = _resolve_output_script_path(args)
|
|
221
|
+
|
|
222
|
+
log.info("=" * 60)
|
|
223
|
+
log.info("Starting ScreenForge UI automation engine")
|
|
224
|
+
target_label = (
|
|
225
|
+
args.goal
|
|
226
|
+
or getattr(args, "action_name", "")
|
|
227
|
+
or getattr(args, "action", "")
|
|
228
|
+
or args.workflow
|
|
229
|
+
or "doctor / no-goal mode"
|
|
230
|
+
)
|
|
231
|
+
log.info(f"Target: {target_label}")
|
|
232
|
+
log.info(f"Circuit breaker: max {args.max_retries} retries per step")
|
|
233
|
+
log.info(
|
|
234
|
+
f"Platform: {args.platform} | Vision: {'on' if args.vision else 'off'}"
|
|
235
|
+
)
|
|
236
|
+
log.info(f"Mode: {execution_mode}")
|
|
237
|
+
log.info(f"Output: {output_script_path}")
|
|
238
|
+
log.info("=" * 60)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
context_content, resume_context = _load_context_content(args)
|
|
242
|
+
except RunContextLoadError as e:
|
|
243
|
+
log.error(f"[E011] Failed to restore run context: {e}. Fix: check that report/runs/<run_id>/ exists and contains summary.json")
|
|
244
|
+
sys.exit(2)
|
|
245
|
+
|
|
246
|
+
if _requires_model_runtime(args, execution_mode) and not config.validate_config():
|
|
247
|
+
log.error("[E012] Configuration validation failed. See errors above for details and fix instructions")
|
|
248
|
+
sys.exit(1)
|
|
249
|
+
|
|
250
|
+
exit_code = _dispatch_execution(
|
|
251
|
+
args,
|
|
252
|
+
execution_mode,
|
|
253
|
+
output_script_path,
|
|
254
|
+
context_content,
|
|
255
|
+
resume_context,
|
|
256
|
+
shared_adapter_manager=shared_adapter_mgr,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if session_id and args.action:
|
|
260
|
+
from cli.session import load_session, update_session
|
|
261
|
+
|
|
262
|
+
session = load_session(session_id)
|
|
263
|
+
if session and exit_code == 0:
|
|
264
|
+
update_session(session_id, steps=session.get("steps", 0) + 1)
|
|
265
|
+
|
|
266
|
+
sys.exit(exit_code)
|