screenforge 0.4.0__py3-none-any.whl

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 (64) hide show
  1. cli/__init__.py +0 -0
  2. cli/_version.py +1 -0
  3. cli/dispatch.py +266 -0
  4. cli/doctor.py +487 -0
  5. cli/modes/__init__.py +0 -0
  6. cli/modes/action.py +262 -0
  7. cli/modes/default.py +248 -0
  8. cli/modes/demo.py +162 -0
  9. cli/modes/dry_run.py +237 -0
  10. cli/modes/init.py +133 -0
  11. cli/modes/plan.py +148 -0
  12. cli/modes/workflow.py +354 -0
  13. cli/parser.py +305 -0
  14. cli/reporter.py +207 -0
  15. cli/session.py +146 -0
  16. cli/shared.py +427 -0
  17. cli/shorthand.py +90 -0
  18. cli/tool_protocol_handlers.py +446 -0
  19. common/__init__.py +0 -0
  20. common/adapters/__init__.py +21 -0
  21. common/adapters/android_adapter.py +273 -0
  22. common/adapters/base_adapter.py +24 -0
  23. common/adapters/ios_adapter.py +278 -0
  24. common/adapters/web_adapter.py +271 -0
  25. common/ai.py +277 -0
  26. common/ai_autonomous.py +273 -0
  27. common/ai_heal.py +222 -0
  28. common/cache/__init__.py +15 -0
  29. common/cache/cache_hash.py +57 -0
  30. common/cache/cache_manager.py +300 -0
  31. common/cache/cache_stats.py +133 -0
  32. common/cache/cache_storage.py +79 -0
  33. common/cache/embedding_loader.py +150 -0
  34. common/capabilities.py +121 -0
  35. common/case_memory.py +327 -0
  36. common/error_codes.py +61 -0
  37. common/exceptions.py +18 -0
  38. common/executor.py +1504 -0
  39. common/failure_diagnosis.py +138 -0
  40. common/history_manager.py +75 -0
  41. common/logs.py +168 -0
  42. common/mcp_server.py +467 -0
  43. common/preflight.py +496 -0
  44. common/progress.py +37 -0
  45. common/run_reporter.py +415 -0
  46. common/run_resume.py +149 -0
  47. common/runtime_modes.py +35 -0
  48. common/tool_protocol.py +196 -0
  49. common/visual_fallback.py +71 -0
  50. common/workflow_schema.py +150 -0
  51. config/__init__.py +0 -0
  52. config/config.py +167 -0
  53. config/env_loader.py +76 -0
  54. screenforge-0.4.0.dist-info/METADATA +43 -0
  55. screenforge-0.4.0.dist-info/RECORD +64 -0
  56. screenforge-0.4.0.dist-info/WHEEL +5 -0
  57. screenforge-0.4.0.dist-info/entry_points.txt +2 -0
  58. screenforge-0.4.0.dist-info/licenses/LICENSE +21 -0
  59. screenforge-0.4.0.dist-info/top_level.txt +4 -0
  60. utils/__init__.py +0 -0
  61. utils/screenshot_annotator.py +60 -0
  62. utils/utils_ios.py +195 -0
  63. utils/utils_web.py +304 -0
  64. utils/utils_xml.py +218 -0
cli/modes/dry_run.py ADDED
@@ -0,0 +1,237 @@
1
+ """Dry-run execution modes."""
2
+
3
+ import cli.shared as _shared
4
+ from cli.reporter import (
5
+ _apply_resume_summary,
6
+ _build_action_summary,
7
+ _build_inline_action_data,
8
+ _build_reporter,
9
+ _emit_run_started,
10
+ )
11
+ from cli.shared import (
12
+ _capture_ui_state,
13
+ _connect_adapter,
14
+ _ensure_executor_runtime,
15
+ _ensure_runtime_classes,
16
+ log,
17
+ )
18
+ from common.runtime_modes import MODE_DRY_RUN
19
+
20
+
21
+ def _preview_action_resolution(device, platform: str, action_data: dict) -> dict:
22
+ _ensure_executor_runtime()
23
+ l_type = action_data.get("locator_type", "")
24
+ l_value = action_data.get("locator_value", "")
25
+ if not l_type or str(l_type).lower() == "global" or str(l_value).lower() == "global":
26
+ return {"resolvable": True, "resolution_error": ""}
27
+
28
+ u2_locator_map = {
29
+ "resourceId": "resourceId",
30
+ "text": "text",
31
+ "description": "description",
32
+ "id": "resourceId",
33
+ }
34
+ u2_key = u2_locator_map.get(l_type, l_type)
35
+ try:
36
+ element = _shared.get_actual_element(device, platform, u2_key, l_value)
37
+ return {"resolvable": element is not None, "resolution_error": ""}
38
+ except Exception as e:
39
+ return {"resolvable": False, "resolution_error": str(e)}
40
+
41
+
42
+ def _build_resolution_hint(args, action_data: dict, resolution: dict) -> str:
43
+ if resolution.get("resolvable", False):
44
+ return ""
45
+
46
+ locator_type = action_data.get("locator_type", "")
47
+ if not args.vision and str(locator_type).lower() != "global":
48
+ return "Locator resolution failed. Verify current page state; consider enabling --vision."
49
+ return "Locator resolution failed. Verify page structure and whether the target element exists."
50
+
51
+
52
+ def run_dry_run_mode(
53
+ args,
54
+ output_script_path: str,
55
+ context_content: str,
56
+ resume_context: dict,
57
+ ) -> int:
58
+ reporter = _build_reporter(args, output_script_path, MODE_DRY_RUN)
59
+ final_status = "failed"
60
+ exit_code = 1
61
+ final_error = ""
62
+ adapter = None
63
+ _emit_run_started(reporter, args, output_script_path, MODE_DRY_RUN)
64
+ _apply_resume_summary(reporter, resume_context)
65
+
66
+ try:
67
+ adapter = _connect_adapter(args, reporter)
68
+ ui_json, screenshot_base64 = _capture_ui_state(args, adapter, reporter, 1)
69
+ _ensure_runtime_classes()
70
+ brain = _shared.AutonomousBrain()
71
+ decision_data = brain.get_next_autonomous_action(
72
+ goal=args.goal,
73
+ context=context_content,
74
+ ui_json=ui_json,
75
+ history=[],
76
+ platform=args.platform,
77
+ last_error="",
78
+ screenshot_base64=screenshot_base64,
79
+ )
80
+
81
+ status = decision_data.get("status", "failed")
82
+ action_data = decision_data.get("result", {})
83
+ resolution = _preview_action_resolution(
84
+ adapter.driver, args.platform, action_data
85
+ )
86
+ resolution_hint = _build_resolution_hint(args, action_data, resolution)
87
+ reporter.emit_event(
88
+ "dry_run_preview",
89
+ status=status,
90
+ action=action_data.get("action", ""),
91
+ locator_type=action_data.get("locator_type", ""),
92
+ locator_value=action_data.get("locator_value", ""),
93
+ extra_value=action_data.get("extra_value", ""),
94
+ resolvable=resolution.get("resolvable", False),
95
+ resolution_error=resolution.get("resolution_error", ""),
96
+ resolution_hint=resolution_hint,
97
+ )
98
+ reporter.update_summary(
99
+ dry_run_preview={
100
+ "status": status,
101
+ "action": action_data.get("action", ""),
102
+ "locator_type": action_data.get("locator_type", ""),
103
+ "locator_value": action_data.get("locator_value", ""),
104
+ "extra_value": action_data.get("extra_value", ""),
105
+ "resolvable": resolution.get("resolvable", False),
106
+ "resolution_error": resolution.get("resolution_error", ""),
107
+ "resolution_hint": resolution_hint,
108
+ }
109
+ )
110
+
111
+ if status == "failed":
112
+ final_error = "Task cannot continue — AI determined failure"
113
+ log.warning("⚠️ [Dry Run] AI determined task cannot continue.")
114
+ exit_code = 1
115
+ else:
116
+ log.info(
117
+ f"🧪 [Dry Run] would_execute: {action_data.get('action', '')} "
118
+ f"{action_data.get('locator_type', '')}={action_data.get('locator_value', '')}"
119
+ )
120
+ final_status = "success"
121
+ exit_code = 0
122
+ except Exception as e:
123
+ final_error = str(e)
124
+ reporter.emit_event("dry_run_failed", error=str(e))
125
+ log.error(f"❌ [Dry Run] Simulation failed: {e}")
126
+ finally:
127
+ reporter.finalize(
128
+ status=final_status,
129
+ exit_code=exit_code,
130
+ steps_executed=1 if not final_error else 0,
131
+ last_error=final_error,
132
+ )
133
+ if adapter:
134
+ try:
135
+ adapter.teardown()
136
+ except Exception as e:
137
+ log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
138
+ return exit_code
139
+
140
+
141
+ def run_action_dry_run_mode(
142
+ args,
143
+ output_script_path: str,
144
+ resume_context: dict,
145
+ ) -> int:
146
+ reporter = _build_reporter(args, output_script_path, MODE_DRY_RUN)
147
+ final_status = "failed"
148
+ exit_code = 1
149
+ final_error = ""
150
+ adapter = None
151
+ preview_steps = []
152
+ _emit_run_started(reporter, args, output_script_path, MODE_DRY_RUN)
153
+ _apply_resume_summary(reporter, resume_context)
154
+
155
+ try:
156
+ action_data = _build_inline_action_data(args)
157
+ action_summary = _build_action_summary(args, action_data)
158
+ reporter.update_control_summary(
159
+ control_kind="action",
160
+ control_label=action_summary["action_name"],
161
+ source_ref="inline://action",
162
+ action=action_summary["action"],
163
+ locator_type=action_summary["locator_type"],
164
+ locator_value=action_summary["locator_value"],
165
+ extra_value=action_summary["extra_value"],
166
+ )
167
+ reporter.emit_event(
168
+ "action_loaded",
169
+ action_name=action_summary["action_name"],
170
+ action=action_summary["action"],
171
+ locator_type=action_summary["locator_type"],
172
+ locator_value=action_summary["locator_value"],
173
+ )
174
+
175
+ adapter = _connect_adapter(args, reporter)
176
+ resolution = _preview_action_resolution(
177
+ adapter.driver, args.platform, action_data
178
+ )
179
+ resolution_hint = _build_resolution_hint(args, action_data, resolution)
180
+ preview = {
181
+ "step": 1,
182
+ "name": action_data["name"],
183
+ "action": action_data["action"],
184
+ "locator_type": action_data["locator_type"],
185
+ "locator_value": action_data["locator_value"],
186
+ "extra_value": action_data["extra_value"],
187
+ "resolvable": resolution.get("resolvable", False),
188
+ "resolution_error": resolution.get("resolution_error", ""),
189
+ "resolution_hint": resolution_hint,
190
+ }
191
+ preview_steps.append(preview)
192
+ reporter.emit_event("action_step_preview", **preview)
193
+
194
+ reporter.update_summary(
195
+ action_summary=_build_action_summary(
196
+ args,
197
+ action_data,
198
+ resolvable=preview["resolvable"],
199
+ resolution_error=preview["resolution_error"],
200
+ resolution_hint=preview["resolution_hint"],
201
+ ),
202
+ dry_run_preview={
203
+ "workflow": False,
204
+ "step_count": 1,
205
+ "unresolved_steps": 0 if preview["resolvable"] else 1,
206
+ "preview_steps": preview_steps,
207
+ },
208
+ )
209
+ reporter.update_control_summary(
210
+ resolvable=preview["resolvable"],
211
+ resolution_error=preview["resolution_error"],
212
+ resolution_hint=preview["resolution_hint"],
213
+ )
214
+
215
+ if preview["resolvable"]:
216
+ final_status = "success"
217
+ exit_code = 0
218
+ else:
219
+ final_error = "Action locator cannot be resolved"
220
+ exit_code = 1
221
+ except Exception as e:
222
+ final_error = str(e)
223
+ reporter.emit_event("action_dry_run_failed", error=str(e))
224
+ log.error(f"❌ [Action] Dry-run failed: {e}")
225
+ finally:
226
+ reporter.finalize(
227
+ status=final_status,
228
+ exit_code=exit_code,
229
+ steps_executed=len(preview_steps),
230
+ last_error=final_error,
231
+ )
232
+ if adapter:
233
+ try:
234
+ adapter.teardown()
235
+ except Exception as e:
236
+ log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
237
+ return exit_code
cli/modes/init.py ADDED
@@ -0,0 +1,133 @@
1
+ """Interactive init wizard: guides new users through first-time setup."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def _prompt_choice(question: str, options: list[str], default: int = 0) -> int:
7
+ print(f"\n {question}")
8
+ for i, opt in enumerate(options):
9
+ marker = ">" if i == default else " "
10
+ print(f" {marker} [{i + 1}] {opt}")
11
+ while True:
12
+ raw = input(f" Choice [{default + 1}]: ").strip()
13
+ if not raw:
14
+ return default
15
+ try:
16
+ choice = int(raw) - 1
17
+ if 0 <= choice < len(options):
18
+ return choice
19
+ except ValueError:
20
+ pass
21
+ print(f" Please enter 1-{len(options)}")
22
+
23
+
24
+ def _prompt_input(question: str, default: str = "") -> str:
25
+ suffix = f" [{default}]" if default else ""
26
+ raw = input(f" {question}{suffix}: ").strip()
27
+ return raw or default
28
+
29
+
30
+ def run_init_mode() -> int:
31
+ print("\n screenforge init — First-time setup wizard")
32
+ print(" " + "=" * 44)
33
+
34
+ # Step 1: Platform
35
+ platform_idx = _prompt_choice(
36
+ "Which platform will you test?",
37
+ ["Web (Chrome/Playwright)", "Android (uiautomator2)", "iOS (WebDriverAgent)"],
38
+ default=0,
39
+ )
40
+ platform = ["web", "android", "ios"][platform_idx]
41
+
42
+ # Step 2: LLM config
43
+ print("\n ScreenForge needs an LLM API key for AI-driven testing.")
44
+ print(" (Skip this for --demo mode which requires no key)")
45
+
46
+ api_key = _prompt_input("OPENAI_API_KEY (or compatible)", "sk-...")
47
+ base_url = _prompt_input("OPENAI_BASE_URL", "https://api.openai.com/v1")
48
+ model = _prompt_input("MODEL_NAME", "gpt-4o")
49
+
50
+ # Step 3: Write .env
51
+ env_path = Path(".env")
52
+ if env_path.exists():
53
+ overwrite = _prompt_choice(
54
+ ".env already exists. Overwrite?",
55
+ ["No, keep existing", "Yes, overwrite"],
56
+ default=0,
57
+ )
58
+ if overwrite == 0:
59
+ print(" Keeping existing .env")
60
+ else:
61
+ _write_env(env_path, api_key, base_url, model)
62
+ else:
63
+ _write_env(env_path, api_key, base_url, model)
64
+
65
+ # Step 4: Platform-specific checks
66
+ print(f"\n Platform: {platform}")
67
+ if platform == "web":
68
+ _check_web_deps()
69
+ elif platform == "android":
70
+ _check_android_deps()
71
+ elif platform == "ios":
72
+ _check_ios_deps()
73
+
74
+ # Step 5: Suggest next steps
75
+ print("\n Setup complete! Next steps:")
76
+ print(" " + "-" * 30)
77
+ print(" 1. screenforge --demo # See it work (no API key needed)")
78
+ print(f" 2. screenforge --doctor --platform {platform} # Verify environment")
79
+ print(f" 3. screenforge --action goto --platform {platform} --extra-value \"https://example.com\"")
80
+ print()
81
+
82
+ return 0
83
+
84
+
85
+ def _write_env(path: Path, api_key: str, base_url: str, model: str) -> None:
86
+ content = f"""OPENAI_API_KEY = "{api_key}"
87
+ OPENAI_BASE_URL = "{base_url}"
88
+ MODEL_NAME = "{model}"
89
+ """
90
+ path.write_text(content)
91
+ print(f" Written: {path}")
92
+
93
+
94
+ def _check_web_deps() -> None:
95
+ try:
96
+ import playwright # noqa: F401
97
+ print(" [ok] playwright installed")
98
+ except ImportError:
99
+ print(" [!!] playwright not installed")
100
+ print(" Fix: pip install playwright && playwright install chromium")
101
+ return
102
+
103
+ from shutil import which
104
+ if which("chromium") or which("google-chrome") or which("chrome"):
105
+ print(" [ok] Chrome/Chromium found")
106
+ else:
107
+ print(" [ok] Will use Playwright's bundled Chromium")
108
+
109
+
110
+ def _check_android_deps() -> None:
111
+ from shutil import which
112
+
113
+ if which("adb"):
114
+ print(" [ok] adb found")
115
+ else:
116
+ print(" [!!] adb not found in PATH")
117
+ print(" Fix: install Android SDK platform-tools")
118
+
119
+ try:
120
+ import uiautomator2 # noqa: F401
121
+ print(" [ok] uiautomator2 installed")
122
+ except ImportError:
123
+ print(" [!!] uiautomator2 not installed")
124
+ print(" Fix: pip install screenforge[android]")
125
+
126
+
127
+ def _check_ios_deps() -> None:
128
+ try:
129
+ import wda # noqa: F401
130
+ print(" [ok] facebook-wda installed")
131
+ except ImportError:
132
+ print(" [!!] facebook-wda not installed")
133
+ print(" Fix: pip install screenforge[ios]")
cli/modes/plan.py ADDED
@@ -0,0 +1,148 @@
1
+ """Plan-only execution modes."""
2
+
3
+ import cli.shared as _shared
4
+ from cli.reporter import (
5
+ _apply_resume_summary,
6
+ _build_action_summary,
7
+ _build_inline_action_data,
8
+ _build_reporter,
9
+ _emit_run_started,
10
+ )
11
+ from cli.shared import (
12
+ _capture_ui_state,
13
+ _connect_adapter,
14
+ _ensure_runtime_classes,
15
+ log,
16
+ )
17
+ from common.runtime_modes import MODE_PLAN_ONLY
18
+
19
+
20
+ def run_plan_only_mode(
21
+ args,
22
+ output_script_path: str,
23
+ context_content: str,
24
+ resume_context: dict,
25
+ ) -> int:
26
+ reporter = _build_reporter(args, output_script_path, MODE_PLAN_ONLY)
27
+ final_status = "failed"
28
+ exit_code = 1
29
+ final_error = ""
30
+ adapter = None
31
+ steps_executed = 0
32
+ _emit_run_started(reporter, args, output_script_path, MODE_PLAN_ONLY)
33
+ _apply_resume_summary(reporter, resume_context)
34
+
35
+ try:
36
+ adapter = _connect_adapter(args, reporter)
37
+ ui_json, screenshot_base64 = _capture_ui_state(args, adapter, reporter, 1)
38
+ _ensure_runtime_classes()
39
+ brain = _shared.AutonomousBrain()
40
+ plan = brain.get_execution_plan(
41
+ goal=args.goal,
42
+ context=context_content,
43
+ ui_json=ui_json,
44
+ history=[],
45
+ platform=args.platform,
46
+ screenshot_base64=screenshot_base64,
47
+ )
48
+
49
+ planned_steps = plan.get("planned_steps", [])
50
+ steps_executed = len(planned_steps) or 1
51
+ reporter.emit_event(
52
+ "plan_generated",
53
+ current_state_summary=plan.get("current_state_summary", ""),
54
+ planned_steps=planned_steps,
55
+ suggested_assertion=plan.get("suggested_assertion", ""),
56
+ risks=plan.get("risks", []),
57
+ )
58
+ reporter.update_summary(plan_preview=plan)
59
+ log.info(f"🧭 [Plan] Current page summary: {plan.get('current_state_summary', '')}")
60
+ for index, step in enumerate(planned_steps, start=1):
61
+ log.info(f"🧭 [Plan] Step {index}: {step}")
62
+ if plan.get("suggested_assertion"):
63
+ log.info(f"🧭 [Plan] Suggested assertion: {plan.get('suggested_assertion', '')}")
64
+
65
+ final_status = "success"
66
+ exit_code = 0
67
+ except Exception as e:
68
+ final_error = str(e)
69
+ reporter.emit_event("plan_failed", error=str(e))
70
+ log.error(f"❌ [Plan] Plan generation failed: {e}")
71
+ finally:
72
+ reporter.finalize(
73
+ status=final_status,
74
+ exit_code=exit_code,
75
+ steps_executed=steps_executed,
76
+ last_error=final_error,
77
+ )
78
+ if adapter:
79
+ try:
80
+ adapter.teardown()
81
+ except Exception as e:
82
+ log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
83
+ return exit_code
84
+
85
+
86
+ def run_action_plan_only_mode(
87
+ args,
88
+ output_script_path: str,
89
+ resume_context: dict,
90
+ ) -> int:
91
+ reporter = _build_reporter(args, output_script_path, MODE_PLAN_ONLY)
92
+ final_status = "failed"
93
+ exit_code = 1
94
+ final_error = ""
95
+ _emit_run_started(reporter, args, output_script_path, MODE_PLAN_ONLY)
96
+ _apply_resume_summary(reporter, resume_context)
97
+
98
+ try:
99
+ action_data = _build_inline_action_data(args)
100
+ plan = {
101
+ "current_state_summary": f"Inline action [{action_data['name']}] preview",
102
+ "planned_steps": [action_data["name"]],
103
+ "suggested_assertion": "",
104
+ "risks": [],
105
+ }
106
+ action_summary = _build_action_summary(args, action_data)
107
+ reporter.update_control_summary(
108
+ control_kind="action",
109
+ control_label=action_summary["action_name"],
110
+ source_ref="inline://action",
111
+ action=action_summary["action"],
112
+ locator_type=action_summary["locator_type"],
113
+ locator_value=action_summary["locator_value"],
114
+ extra_value=action_summary["extra_value"],
115
+ )
116
+ reporter.emit_event(
117
+ "action_loaded",
118
+ action_name=action_summary["action_name"],
119
+ action=action_summary["action"],
120
+ locator_type=action_summary["locator_type"],
121
+ locator_value=action_summary["locator_value"],
122
+ )
123
+ reporter.emit_event(
124
+ "plan_generated",
125
+ current_state_summary=plan["current_state_summary"],
126
+ planned_steps=plan["planned_steps"],
127
+ suggested_assertion="",
128
+ risks=[],
129
+ )
130
+ reporter.update_summary(plan_preview=plan, action_summary=action_summary)
131
+
132
+ log.info(f"🧭 [Action] Action name: {action_summary['action_name']}")
133
+ log.info(f"🧭 [Action] Preview step: {action_summary['action_name']}")
134
+
135
+ final_status = "success"
136
+ exit_code = 0
137
+ except Exception as e:
138
+ final_error = str(e)
139
+ reporter.emit_event("action_plan_failed", error=str(e))
140
+ log.error(f"❌ [Action] Plan generation failed: {e}")
141
+ finally:
142
+ reporter.finalize(
143
+ status=final_status,
144
+ exit_code=exit_code,
145
+ steps_executed=1 if final_status == "success" else 0,
146
+ last_error=final_error,
147
+ )
148
+ return exit_code