sentienceapi 0.95.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.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (82) hide show
  1. sentience/__init__.py +253 -0
  2. sentience/_extension_loader.py +195 -0
  3. sentience/action_executor.py +215 -0
  4. sentience/actions.py +1020 -0
  5. sentience/agent.py +1181 -0
  6. sentience/agent_config.py +46 -0
  7. sentience/agent_runtime.py +424 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +108 -0
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +343 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +427 -0
  21. sentience/base_agent.py +196 -0
  22. sentience/browser.py +1215 -0
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cli.py +130 -0
  26. sentience/cloud_tracing.py +807 -0
  27. sentience/constants.py +6 -0
  28. sentience/conversational_agent.py +543 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +188 -0
  31. sentience/extension/background.js +104 -0
  32. sentience/extension/content.js +161 -0
  33. sentience/extension/injected_api.js +914 -0
  34. sentience/extension/manifest.json +36 -0
  35. sentience/extension/pkg/sentience_core.d.ts +51 -0
  36. sentience/extension/pkg/sentience_core.js +323 -0
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  39. sentience/extension/release.json +115 -0
  40. sentience/formatting.py +15 -0
  41. sentience/generator.py +202 -0
  42. sentience/inspector.py +367 -0
  43. sentience/llm_interaction_handler.py +191 -0
  44. sentience/llm_provider.py +875 -0
  45. sentience/llm_provider_utils.py +120 -0
  46. sentience/llm_response_builder.py +153 -0
  47. sentience/models.py +846 -0
  48. sentience/ordinal.py +280 -0
  49. sentience/overlay.py +222 -0
  50. sentience/protocols.py +228 -0
  51. sentience/query.py +303 -0
  52. sentience/read.py +188 -0
  53. sentience/recorder.py +589 -0
  54. sentience/schemas/trace_v1.json +335 -0
  55. sentience/screenshot.py +100 -0
  56. sentience/sentience_methods.py +86 -0
  57. sentience/snapshot.py +706 -0
  58. sentience/snapshot_diff.py +126 -0
  59. sentience/text_search.py +262 -0
  60. sentience/trace_event_builder.py +148 -0
  61. sentience/trace_file_manager.py +197 -0
  62. sentience/trace_indexing/__init__.py +27 -0
  63. sentience/trace_indexing/index_schema.py +199 -0
  64. sentience/trace_indexing/indexer.py +414 -0
  65. sentience/tracer_factory.py +322 -0
  66. sentience/tracing.py +449 -0
  67. sentience/utils/__init__.py +40 -0
  68. sentience/utils/browser.py +46 -0
  69. sentience/utils/element.py +257 -0
  70. sentience/utils/formatting.py +59 -0
  71. sentience/utils.py +296 -0
  72. sentience/verification.py +380 -0
  73. sentience/visual_agent.py +2058 -0
  74. sentience/wait.py +139 -0
  75. sentienceapi-0.95.0.dist-info/METADATA +984 -0
  76. sentienceapi-0.95.0.dist-info/RECORD +82 -0
  77. sentienceapi-0.95.0.dist-info/WHEEL +5 -0
  78. sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
  79. sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
  80. sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
  81. sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
  82. sentienceapi-0.95.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,299 @@
1
+ """
2
+ Browser evaluation helper for common window.sentience API patterns.
3
+
4
+ Consolidates repeated patterns for:
5
+ - Waiting for extension injection
6
+ - Calling window.sentience methods
7
+ - Error handling with diagnostics
8
+ """
9
+
10
+ from typing import Any, Optional, Union
11
+
12
+ from playwright.async_api import Page as AsyncPage
13
+ from playwright.sync_api import Page
14
+
15
+ from .browser import AsyncSentienceBrowser, SentienceBrowser
16
+ from .sentience_methods import SentienceMethod
17
+
18
+
19
+ class BrowserEvaluator:
20
+ """Helper class for common browser evaluation patterns"""
21
+
22
+ @staticmethod
23
+ def wait_for_extension(
24
+ page: Page | AsyncPage,
25
+ timeout_ms: int = 5000,
26
+ ) -> None:
27
+ """
28
+ Wait for window.sentience API to be available.
29
+
30
+ Args:
31
+ page: Playwright Page instance (sync or async)
32
+ timeout_ms: Timeout in milliseconds (default: 5000)
33
+
34
+ Raises:
35
+ RuntimeError: If extension fails to inject within timeout
36
+ """
37
+ if hasattr(page, "wait_for_function"):
38
+ # Sync page
39
+ try:
40
+ page.wait_for_function(
41
+ "typeof window.sentience !== 'undefined'",
42
+ timeout=timeout_ms,
43
+ )
44
+ except Exception as e:
45
+ diag = BrowserEvaluator._gather_diagnostics(page)
46
+ raise RuntimeError(
47
+ f"Sentience extension failed to inject window.sentience API. "
48
+ f"Is the extension loaded? Diagnostics: {diag}"
49
+ ) from e
50
+ else:
51
+ # Async page - should use async version
52
+ raise TypeError("Use wait_for_extension_async for async pages")
53
+
54
+ @staticmethod
55
+ async def wait_for_extension_async(
56
+ page: AsyncPage,
57
+ timeout_ms: int = 5000,
58
+ ) -> None:
59
+ """
60
+ Wait for window.sentience API to be available (async).
61
+
62
+ Args:
63
+ page: Playwright AsyncPage instance
64
+ timeout_ms: Timeout in milliseconds (default: 5000)
65
+
66
+ Raises:
67
+ RuntimeError: If extension fails to inject within timeout
68
+ """
69
+ try:
70
+ await page.wait_for_function(
71
+ "typeof window.sentience !== 'undefined'",
72
+ timeout=timeout_ms,
73
+ )
74
+ except Exception as e:
75
+ diag = await BrowserEvaluator._gather_diagnostics_async(page)
76
+ raise RuntimeError(
77
+ f"Sentience extension failed to inject window.sentience API. "
78
+ f"Is the extension loaded? Diagnostics: {diag}"
79
+ ) from e
80
+
81
+ @staticmethod
82
+ def _gather_diagnostics(page: Page | AsyncPage) -> dict[str, Any]:
83
+ """
84
+ Gather diagnostics about extension state.
85
+
86
+ Args:
87
+ page: Playwright Page instance
88
+
89
+ Returns:
90
+ Dictionary with diagnostic information
91
+ """
92
+ try:
93
+ if hasattr(page, "evaluate"):
94
+ # Sync page
95
+ return page.evaluate(
96
+ """() => ({
97
+ sentience_defined: typeof window.sentience !== 'undefined',
98
+ extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
99
+ url: window.location.href
100
+ })"""
101
+ )
102
+ else:
103
+ return {"error": "Could not gather diagnostics - invalid page type"}
104
+ except Exception:
105
+ return {"error": "Could not gather diagnostics"}
106
+
107
+ @staticmethod
108
+ async def _gather_diagnostics_async(page: AsyncPage) -> dict[str, Any]:
109
+ """
110
+ Gather diagnostics about extension state (async).
111
+
112
+ Args:
113
+ page: Playwright AsyncPage instance
114
+
115
+ Returns:
116
+ Dictionary with diagnostic information
117
+ """
118
+ try:
119
+ return await page.evaluate(
120
+ """() => ({
121
+ sentience_defined: typeof window.sentience !== 'undefined',
122
+ extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
123
+ url: window.location.href
124
+ })"""
125
+ )
126
+ except Exception:
127
+ return {"error": "Could not gather diagnostics"}
128
+
129
+ @staticmethod
130
+ def invoke(
131
+ page: Page,
132
+ method: SentienceMethod | str,
133
+ *args: Any,
134
+ **kwargs: Any,
135
+ ) -> Any:
136
+ """
137
+ Invoke a window.sentience method with error handling (sync).
138
+
139
+ Args:
140
+ page: Playwright Page instance (sync)
141
+ method: SentienceMethod enum value or method name string (e.g., SentienceMethod.SNAPSHOT or "snapshot")
142
+ *args: Positional arguments to pass to the method
143
+ **kwargs: Keyword arguments to pass to the method
144
+
145
+ Returns:
146
+ Result from the method call
147
+
148
+ Raises:
149
+ RuntimeError: If method is not available or call fails
150
+
151
+ Example:
152
+ ```python
153
+ result = BrowserEvaluator.invoke(page, SentienceMethod.SNAPSHOT, limit=50)
154
+ success = BrowserEvaluator.invoke(page, SentienceMethod.CLICK, element_id)
155
+ ```
156
+ """
157
+ # Convert enum to string if needed
158
+ method_name = method.value if isinstance(method, SentienceMethod) else method
159
+
160
+ # Build JavaScript call
161
+ if args and kwargs:
162
+ # Both args and kwargs - use object spread
163
+ js_code = f"""
164
+ (args, kwargs) => {{
165
+ return window.sentience.{method_name}(...args, kwargs);
166
+ }}
167
+ """
168
+ result = page.evaluate(js_code, list(args), kwargs)
169
+ elif args:
170
+ # Only args
171
+ js_code = f"""
172
+ (args) => {{
173
+ return window.sentience.{method_name}(...args);
174
+ }}
175
+ """
176
+ result = page.evaluate(js_code, list(args))
177
+ elif kwargs:
178
+ # Only kwargs - pass as single object
179
+ js_code = f"""
180
+ (options) => {{
181
+ return window.sentience.{method_name}(options);
182
+ }}
183
+ """
184
+ result = page.evaluate(js_code, kwargs)
185
+ else:
186
+ # No arguments
187
+ js_code = f"""
188
+ () => {{
189
+ return window.sentience.{method_name}();
190
+ }}
191
+ """
192
+ result = page.evaluate(js_code)
193
+
194
+ return result
195
+
196
+ @staticmethod
197
+ async def invoke_async(
198
+ page: AsyncPage,
199
+ method: SentienceMethod | str,
200
+ *args: Any,
201
+ **kwargs: Any,
202
+ ) -> Any:
203
+ """
204
+ Invoke a window.sentience method with error handling (async).
205
+
206
+ Args:
207
+ page: Playwright AsyncPage instance
208
+ method: SentienceMethod enum value or method name string (e.g., SentienceMethod.SNAPSHOT or "snapshot")
209
+ *args: Positional arguments to pass to the method
210
+ **kwargs: Keyword arguments to pass to the method
211
+
212
+ Returns:
213
+ Result from the method call
214
+
215
+ Raises:
216
+ RuntimeError: If method is not available or call fails
217
+
218
+ Example:
219
+ ```python
220
+ result = await BrowserEvaluator.invoke_async(page, SentienceMethod.SNAPSHOT, limit=50)
221
+ success = await BrowserEvaluator.invoke_async(page, SentienceMethod.CLICK, element_id)
222
+ ```
223
+ """
224
+ # Convert enum to string if needed
225
+ method_name = method.value if isinstance(method, SentienceMethod) else method
226
+
227
+ # Build JavaScript call
228
+ if args and kwargs:
229
+ js_code = f"""
230
+ (args, kwargs) => {{
231
+ return window.sentience.{method_name}(...args, kwargs);
232
+ }}
233
+ """
234
+ result = await page.evaluate(js_code, list(args), kwargs)
235
+ elif args:
236
+ js_code = f"""
237
+ (args) => {{
238
+ return window.sentience.{method_name}(...args);
239
+ }}
240
+ """
241
+ result = await page.evaluate(js_code, list(args))
242
+ elif kwargs:
243
+ js_code = f"""
244
+ (options) => {{
245
+ return window.sentience.{method_name}(options);
246
+ }}
247
+ """
248
+ result = await page.evaluate(js_code, kwargs)
249
+ else:
250
+ js_code = f"""
251
+ () => {{
252
+ return window.sentience.{method_name}();
253
+ }}
254
+ """
255
+ result = await page.evaluate(js_code)
256
+
257
+ return result
258
+
259
+ @staticmethod
260
+ def verify_method_exists(
261
+ page: Page,
262
+ method: SentienceMethod | str,
263
+ ) -> bool:
264
+ """
265
+ Verify that a window.sentience method exists.
266
+
267
+ Args:
268
+ page: Playwright Page instance (sync)
269
+ method: SentienceMethod enum value or method name string
270
+
271
+ Returns:
272
+ True if method exists, False otherwise
273
+ """
274
+ method_name = method.value if isinstance(method, SentienceMethod) else method
275
+ try:
276
+ return page.evaluate(f"typeof window.sentience.{method_name} !== 'undefined'")
277
+ except Exception:
278
+ return False
279
+
280
+ @staticmethod
281
+ async def verify_method_exists_async(
282
+ page: AsyncPage,
283
+ method: SentienceMethod | str,
284
+ ) -> bool:
285
+ """
286
+ Verify that a window.sentience method exists (async).
287
+
288
+ Args:
289
+ page: Playwright AsyncPage instance
290
+ method: SentienceMethod enum value or method name string
291
+
292
+ Returns:
293
+ True if method exists, False otherwise
294
+ """
295
+ method_name = method.value if isinstance(method, SentienceMethod) else method
296
+ try:
297
+ return await page.evaluate(f"typeof window.sentience.{method_name} !== 'undefined'")
298
+ except Exception:
299
+ return False
@@ -0,0 +1,207 @@
1
+ """
2
+ Shared canonicalization utilities for snapshot comparison and indexing.
3
+
4
+ This module provides consistent normalization functions used by both:
5
+ - trace_indexing/indexer.py (for computing stable digests)
6
+ - snapshot_diff.py (for computing diff_status labels)
7
+
8
+ By sharing these helpers, we ensure consistent behavior:
9
+ - Same text normalization (whitespace, case, length)
10
+ - Same bbox rounding (2px precision)
11
+ - Same change detection thresholds
12
+ """
13
+
14
+ from typing import Any
15
+
16
+
17
+ def normalize_text(text: str | None, max_len: int = 80) -> str:
18
+ """
19
+ Normalize text for canonical comparison.
20
+
21
+ Transforms:
22
+ - Trims leading/trailing whitespace
23
+ - Collapses internal whitespace to single spaces
24
+ - Lowercases
25
+ - Caps length
26
+
27
+ Args:
28
+ text: Input text (may be None)
29
+ max_len: Maximum length to retain (default: 80)
30
+
31
+ Returns:
32
+ Normalized text string (empty string if input is None)
33
+
34
+ Examples:
35
+ >>> normalize_text(" Hello World ")
36
+ 'hello world'
37
+ >>> normalize_text(None)
38
+ ''
39
+ """
40
+ if not text:
41
+ return ""
42
+ # Trim and collapse whitespace
43
+ normalized = " ".join(text.split())
44
+ # Lowercase
45
+ normalized = normalized.lower()
46
+ # Cap length
47
+ if len(normalized) > max_len:
48
+ normalized = normalized[:max_len]
49
+ return normalized
50
+
51
+
52
+ def round_bbox(bbox: dict[str, float], precision: int = 2) -> dict[str, int]:
53
+ """
54
+ Round bbox coordinates to reduce noise.
55
+
56
+ Snaps coordinates to grid of `precision` pixels to ignore
57
+ sub-pixel rendering differences.
58
+
59
+ Args:
60
+ bbox: Bounding box with x, y, width, height
61
+ precision: Grid size in pixels (default: 2)
62
+
63
+ Returns:
64
+ Rounded bbox with integer coordinates
65
+
66
+ Examples:
67
+ >>> round_bbox({"x": 101, "y": 203, "width": 50, "height": 25})
68
+ {'x': 100, 'y': 202, 'width': 50, 'height': 24}
69
+ """
70
+ return {
71
+ "x": round(bbox.get("x", 0) / precision) * precision,
72
+ "y": round(bbox.get("y", 0) / precision) * precision,
73
+ "width": round(bbox.get("width", 0) / precision) * precision,
74
+ "height": round(bbox.get("height", 0) / precision) * precision,
75
+ }
76
+
77
+
78
+ def bbox_equal(bbox1: dict[str, Any], bbox2: dict[str, Any], threshold: float = 5.0) -> bool:
79
+ """
80
+ Check if two bboxes are equal within a threshold.
81
+
82
+ Args:
83
+ bbox1: First bounding box
84
+ bbox2: Second bounding box
85
+ threshold: Maximum allowed difference in pixels (default: 5.0)
86
+
87
+ Returns:
88
+ True if all bbox properties differ by less than threshold
89
+
90
+ Examples:
91
+ >>> bbox_equal({"x": 100, "y": 200, "width": 50, "height": 25},
92
+ ... {"x": 102, "y": 200, "width": 50, "height": 25})
93
+ True # 2px difference is below 5px threshold
94
+ """
95
+ return (
96
+ abs(bbox1.get("x", 0) - bbox2.get("x", 0)) <= threshold
97
+ and abs(bbox1.get("y", 0) - bbox2.get("y", 0)) <= threshold
98
+ and abs(bbox1.get("width", 0) - bbox2.get("width", 0)) <= threshold
99
+ and abs(bbox1.get("height", 0) - bbox2.get("height", 0)) <= threshold
100
+ )
101
+
102
+
103
+ def bbox_changed(bbox1: dict[str, Any], bbox2: dict[str, Any], threshold: float = 5.0) -> bool:
104
+ """
105
+ Check if two bboxes differ beyond the threshold.
106
+
107
+ This is the inverse of bbox_equal, provided for semantic clarity
108
+ in diff detection code.
109
+
110
+ Args:
111
+ bbox1: First bounding box
112
+ bbox2: Second bounding box
113
+ threshold: Maximum allowed difference in pixels (default: 5.0)
114
+
115
+ Returns:
116
+ True if any bbox property differs by more than threshold
117
+ """
118
+ return not bbox_equal(bbox1, bbox2, threshold)
119
+
120
+
121
+ def canonicalize_element(elem: dict[str, Any]) -> dict[str, Any]:
122
+ """
123
+ Create canonical representation of an element for comparison/hashing.
124
+
125
+ Extracts and normalizes the fields that matter for identity:
126
+ - id, role, normalized text, rounded bbox
127
+ - is_primary, is_clickable from visual_cues
128
+
129
+ Args:
130
+ elem: Raw element dictionary
131
+
132
+ Returns:
133
+ Canonical element dictionary with normalized fields
134
+
135
+ Examples:
136
+ >>> canonicalize_element({
137
+ ... "id": 1,
138
+ ... "role": "button",
139
+ ... "text": " Click Me ",
140
+ ... "bbox": {"x": 101, "y": 200, "width": 50, "height": 25},
141
+ ... "visual_cues": {"is_primary": True, "is_clickable": True}
142
+ ... })
143
+ {'id': 1, 'role': 'button', 'text_norm': 'click me', 'bbox': {'x': 100, 'y': 200, 'width': 50, 'height': 24}, 'is_primary': True, 'is_clickable': True}
144
+ """
145
+ # Extract is_primary and is_clickable from visual_cues if present
146
+ visual_cues = elem.get("visual_cues", {})
147
+ is_primary = (
148
+ visual_cues.get("is_primary", False)
149
+ if isinstance(visual_cues, dict)
150
+ else elem.get("is_primary", False)
151
+ )
152
+ is_clickable = (
153
+ visual_cues.get("is_clickable", False)
154
+ if isinstance(visual_cues, dict)
155
+ else elem.get("is_clickable", False)
156
+ )
157
+
158
+ return {
159
+ "id": elem.get("id"),
160
+ "role": elem.get("role", ""),
161
+ "text_norm": normalize_text(elem.get("text")),
162
+ "bbox": round_bbox(elem.get("bbox", {"x": 0, "y": 0, "width": 0, "height": 0})),
163
+ "is_primary": is_primary,
164
+ "is_clickable": is_clickable,
165
+ }
166
+
167
+
168
+ def content_equal(elem1: dict[str, Any], elem2: dict[str, Any]) -> bool:
169
+ """
170
+ Check if two elements have equal content (ignoring position).
171
+
172
+ Compares normalized text, role, and visual cues.
173
+
174
+ Args:
175
+ elem1: First element (raw or canonical)
176
+ elem2: Second element (raw or canonical)
177
+
178
+ Returns:
179
+ True if content is equal after normalization
180
+ """
181
+ # Normalize both elements
182
+ c1 = canonicalize_element(elem1)
183
+ c2 = canonicalize_element(elem2)
184
+
185
+ return (
186
+ c1["role"] == c2["role"]
187
+ and c1["text_norm"] == c2["text_norm"]
188
+ and c1["is_primary"] == c2["is_primary"]
189
+ and c1["is_clickable"] == c2["is_clickable"]
190
+ )
191
+
192
+
193
+ def content_changed(elem1: dict[str, Any], elem2: dict[str, Any]) -> bool:
194
+ """
195
+ Check if two elements have different content (ignoring position).
196
+
197
+ This is the inverse of content_equal, provided for semantic clarity
198
+ in diff detection code.
199
+
200
+ Args:
201
+ elem1: First element
202
+ elem2: Second element
203
+
204
+ Returns:
205
+ True if content differs after normalization
206
+ """
207
+ return not content_equal(elem1, elem2)
sentience/cli.py ADDED
@@ -0,0 +1,130 @@
1
+ """
2
+ CLI commands for Sentience SDK
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from .browser import SentienceBrowser
9
+ from .generator import ScriptGenerator
10
+ from .inspector import inspect
11
+ from .recorder import Trace, record
12
+
13
+
14
+ def cmd_inspect(args):
15
+ """Start inspector mode"""
16
+ browser = SentienceBrowser(headless=False)
17
+ try:
18
+ browser.start()
19
+ print("āœ… Inspector started. Hover elements to see info, click to see full details.")
20
+ print("Press Ctrl+C to stop.")
21
+
22
+ with inspect(browser):
23
+ # Keep running until interrupted
24
+ import time
25
+
26
+ try:
27
+ while True:
28
+ time.sleep(1)
29
+ except KeyboardInterrupt:
30
+ print("\nšŸ‘‹ Inspector stopped.")
31
+ finally:
32
+ browser.close()
33
+
34
+
35
+ def cmd_record(args):
36
+ """Start recording mode"""
37
+ browser = SentienceBrowser(headless=False)
38
+ try:
39
+ browser.start()
40
+
41
+ # Navigate to start URL if provided
42
+ if args.url:
43
+ browser.page.goto(args.url)
44
+ browser.page.wait_for_load_state("networkidle")
45
+
46
+ print("āœ… Recording started. Perform actions in the browser.")
47
+ print("Press Ctrl+C to stop and save trace.")
48
+
49
+ with record(browser, capture_snapshots=args.snapshots) as rec:
50
+ # Add mask patterns if provided
51
+ for pattern in args.mask or []:
52
+ rec.add_mask_pattern(pattern)
53
+
54
+ # Keep running until interrupted
55
+ import time
56
+
57
+ try:
58
+ while True:
59
+ time.sleep(1)
60
+ except KeyboardInterrupt:
61
+ print("\nšŸ’¾ Saving trace...")
62
+ output = args.output or "trace.json"
63
+ rec.save(output)
64
+ print(f"āœ… Trace saved to {output}")
65
+ finally:
66
+ browser.close()
67
+
68
+
69
+ def cmd_gen(args):
70
+ """Generate script from trace"""
71
+ # Load trace
72
+ trace = Trace.load(args.trace)
73
+
74
+ # Generate script
75
+ generator = ScriptGenerator(trace)
76
+
77
+ if args.lang == "py":
78
+ output = args.output or "generated.py"
79
+ generator.save_python(output)
80
+ elif args.lang == "ts":
81
+ output = args.output or "generated.ts"
82
+ generator.save_typescript(output)
83
+ else:
84
+ print(f"āŒ Unsupported language: {args.lang}")
85
+ sys.exit(1)
86
+
87
+ print(f"āœ… Generated {args.lang.upper()} script: {output}")
88
+
89
+
90
+ def main():
91
+ """Main CLI entry point"""
92
+ parser = argparse.ArgumentParser(description="Sentience SDK CLI")
93
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
94
+
95
+ # Inspect command
96
+ inspect_parser = subparsers.add_parser("inspect", help="Start inspector mode")
97
+ inspect_parser.set_defaults(func=cmd_inspect)
98
+
99
+ # Record command
100
+ record_parser = subparsers.add_parser("record", help="Start recording mode")
101
+ record_parser.add_argument("--url", help="Start URL")
102
+ record_parser.add_argument("--output", "-o", help="Output trace file", default="trace.json")
103
+ record_parser.add_argument(
104
+ "--snapshots", action="store_true", help="Capture snapshots at each step"
105
+ )
106
+ record_parser.add_argument(
107
+ "--mask",
108
+ action="append",
109
+ help="Pattern to mask in recorded text (e.g., password)",
110
+ )
111
+ record_parser.set_defaults(func=cmd_record)
112
+
113
+ # Generate command
114
+ gen_parser = subparsers.add_parser("gen", help="Generate script from trace")
115
+ gen_parser.add_argument("trace", help="Trace JSON file")
116
+ gen_parser.add_argument("--lang", choices=["py", "ts"], default="py", help="Output language")
117
+ gen_parser.add_argument("--output", "-o", help="Output script file")
118
+ gen_parser.set_defaults(func=cmd_gen)
119
+
120
+ args = parser.parse_args()
121
+
122
+ if not args.command:
123
+ parser.print_help()
124
+ sys.exit(1)
125
+
126
+ args.func(args)
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()