Skip to content

Commit 394e14e

Browse files
committed
all tests passed
1 parent f1e726f commit 394e14e

19 files changed

+1547
-38
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ dependencies = [
1414
"jsonschema>=4.0.0",
1515
]
1616

17+
[project.scripts]
18+
sentience = "sentience.cli:main"
19+
1720
[project.optional-dependencies]
1821
dev = [
1922
"pytest>=7.0.0",

sentience/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
from .browser import SentienceBrowser
66
from .models import Snapshot, Element, BBox, Viewport, ActionResult, WaitResult
7+
from .snapshot import snapshot
78
from .query import query, find
89
from .actions import click, type_text, press
910
from .wait import wait_for
1011
from .expect import expect
12+
from .inspector import Inspector, inspect
13+
from .recorder import Recorder, Trace, TraceStep, record
14+
from .generator import ScriptGenerator, generate
1115

1216
__version__ = "0.1.0"
1317

@@ -19,12 +23,21 @@
1923
"Viewport",
2024
"ActionResult",
2125
"WaitResult",
26+
"snapshot",
2227
"query",
2328
"find",
2429
"click",
2530
"type_text",
2631
"press",
2732
"wait_for",
2833
"expect",
34+
"Inspector",
35+
"inspect",
36+
"Recorder",
37+
"Trace",
38+
"TraceStep",
39+
"record",
40+
"ScriptGenerator",
41+
"generate",
2942
]
3043

393 Bytes
Binary file not shown.
-559 Bytes
Binary file not shown.

sentience/cli.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
CLI commands for Sentience SDK
3+
"""
4+
5+
import argparse
6+
import sys
7+
from pathlib import Path
8+
from .browser import SentienceBrowser
9+
from .inspector import inspect
10+
from .recorder import record
11+
from .generator import ScriptGenerator
12+
from .recorder import Trace
13+
14+
15+
def cmd_inspect(args):
16+
"""Start inspector mode"""
17+
browser = SentienceBrowser(headless=False)
18+
try:
19+
browser.start()
20+
print("✅ Inspector started. Hover elements to see info, click to see full details.")
21+
print("Press Ctrl+C to stop.")
22+
23+
with inspect(browser):
24+
# Keep running until interrupted
25+
import time
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+
try:
57+
while True:
58+
time.sleep(1)
59+
except KeyboardInterrupt:
60+
print("\n💾 Saving trace...")
61+
output = args.output or "trace.json"
62+
rec.save(output)
63+
print(f"✅ Trace saved to {output}")
64+
finally:
65+
browser.close()
66+
67+
68+
def cmd_gen(args):
69+
"""Generate script from trace"""
70+
# Load trace
71+
trace = Trace.load(args.trace)
72+
73+
# Generate script
74+
generator = ScriptGenerator(trace)
75+
76+
if args.lang == "py":
77+
code = generator.generate_python()
78+
output = args.output or "generated.py"
79+
generator.save_python(output)
80+
elif args.lang == "ts":
81+
code = generator.generate_typescript()
82+
output = args.output or "generated.ts"
83+
generator.save_typescript(output)
84+
else:
85+
print(f"❌ Unsupported language: {args.lang}")
86+
sys.exit(1)
87+
88+
print(f"✅ Generated {args.lang.upper()} script: {output}")
89+
90+
91+
def main():
92+
"""Main CLI entry point"""
93+
parser = argparse.ArgumentParser(description="Sentience SDK CLI")
94+
subparsers = parser.add_subparsers(dest="command", help="Commands")
95+
96+
# Inspect command
97+
inspect_parser = subparsers.add_parser("inspect", help="Start inspector mode")
98+
inspect_parser.set_defaults(func=cmd_inspect)
99+
100+
# Record command
101+
record_parser = subparsers.add_parser("record", help="Start recording mode")
102+
record_parser.add_argument("--url", help="Start URL")
103+
record_parser.add_argument("--output", "-o", help="Output trace file", default="trace.json")
104+
record_parser.add_argument("--snapshots", action="store_true", help="Capture snapshots at each step")
105+
record_parser.add_argument("--mask", action="append", help="Pattern to mask in recorded text (e.g., password)")
106+
record_parser.set_defaults(func=cmd_record)
107+
108+
# Generate command
109+
gen_parser = subparsers.add_parser("gen", help="Generate script from trace")
110+
gen_parser.add_argument("trace", help="Trace JSON file")
111+
gen_parser.add_argument("--lang", choices=["py", "ts"], default="py", help="Output language")
112+
gen_parser.add_argument("--output", "-o", help="Output script file")
113+
gen_parser.set_defaults(func=cmd_gen)
114+
115+
args = parser.parse_args()
116+
117+
if not args.command:
118+
parser.print_help()
119+
sys.exit(1)
120+
121+
args.func(args)
122+
123+
124+
if __name__ == "__main__":
125+
main()
126+

sentience/generator.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
"""
2+
Script Generator - converts trace into executable code
3+
"""
4+
5+
import json
6+
from typing import List, Optional
7+
from .recorder import Trace, TraceStep
8+
from .query import find
9+
10+
11+
class ScriptGenerator:
12+
"""Generates Python or TypeScript code from a trace"""
13+
14+
def __init__(self, trace: Trace):
15+
self.trace = trace
16+
17+
def generate_python(self) -> str:
18+
"""Generate Python script from trace"""
19+
lines = [
20+
'"""',
21+
f'Generated script from trace: {self.trace.start_url}',
22+
f'Created: {self.trace.created_at}',
23+
'"""',
24+
'',
25+
'from sentience import SentienceBrowser, snapshot, find, click, type_text, press',
26+
'',
27+
'def main():',
28+
' with SentienceBrowser(headless=False) as browser:',
29+
f' browser.page.goto("{self.trace.start_url}")',
30+
' browser.page.wait_for_load_state("networkidle")',
31+
'',
32+
]
33+
34+
for step in self.trace.steps:
35+
lines.extend(self._generate_python_step(step, indent=' '))
36+
37+
lines.extend([
38+
'',
39+
'if __name__ == "__main__":',
40+
' main()',
41+
])
42+
43+
return '\n'.join(lines)
44+
45+
def generate_typescript(self) -> str:
46+
"""Generate TypeScript script from trace"""
47+
lines = [
48+
'/**',
49+
f' * Generated script from trace: {self.trace.start_url}',
50+
f' * Created: {self.trace.created_at}',
51+
' */',
52+
'',
53+
"import { SentienceBrowser, snapshot, find, click, typeText, press } from './src';",
54+
'',
55+
'async function main() {',
56+
' const browser = new SentienceBrowser(undefined, false);',
57+
'',
58+
' try {',
59+
' await browser.start();',
60+
f' await browser.getPage().goto(\'{self.trace.start_url}\');',
61+
' await browser.getPage().waitForLoadState(\'networkidle\');',
62+
'',
63+
]
64+
65+
for step in self.trace.steps:
66+
lines.extend(self._generate_typescript_step(step, indent=' '))
67+
68+
lines.extend([
69+
' } finally {',
70+
' await browser.close();',
71+
' }',
72+
'}',
73+
'',
74+
'main().catch(console.error);',
75+
])
76+
77+
return '\n'.join(lines)
78+
79+
def _generate_python_step(self, step: TraceStep, indent: str = '') -> List[str]:
80+
"""Generate Python code for a single step"""
81+
lines = []
82+
83+
if step.type == 'navigation':
84+
lines.append(f'{indent}# Navigate to {step.url}')
85+
lines.append(f'{indent}browser.page.goto("{step.url}")')
86+
lines.append(f'{indent}browser.page.wait_for_load_state("networkidle")')
87+
88+
elif step.type == 'click':
89+
if step.selector:
90+
# Use semantic selector
91+
lines.append(f'{indent}# Click: {step.selector}')
92+
lines.append(f'{indent}snap = snapshot(browser)')
93+
lines.append(f'{indent}element = find(snap, "{step.selector}")')
94+
lines.append(f'{indent}if element:')
95+
lines.append(f'{indent} click(browser, element.id)')
96+
lines.append(f'{indent}else:')
97+
lines.append(f'{indent} raise Exception("Element not found: {step.selector}")')
98+
elif step.element_id is not None:
99+
# Fallback to element ID
100+
lines.append(f'{indent}# TODO: replace with semantic selector')
101+
lines.append(f'{indent}click(browser, {step.element_id})')
102+
lines.append('')
103+
104+
elif step.type == 'type':
105+
if step.selector:
106+
lines.append(f'{indent}# Type into: {step.selector}')
107+
lines.append(f'{indent}snap = snapshot(browser)')
108+
lines.append(f'{indent}element = find(snap, "{step.selector}")')
109+
lines.append(f'{indent}if element:')
110+
lines.append(f'{indent} type_text(browser, element.id, "{step.text}")')
111+
lines.append(f'{indent}else:')
112+
lines.append(f'{indent} raise Exception("Element not found: {step.selector}")')
113+
elif step.element_id is not None:
114+
lines.append(f'{indent}# TODO: replace with semantic selector')
115+
lines.append(f'{indent}type_text(browser, {step.element_id}, "{step.text}")')
116+
lines.append('')
117+
118+
elif step.type == 'press':
119+
lines.append(f'{indent}# Press key: {step.key}')
120+
lines.append(f'{indent}press(browser, "{step.key}")')
121+
lines.append('')
122+
123+
return lines
124+
125+
def _generate_typescript_step(self, step: TraceStep, indent: str = '') -> List[str]:
126+
"""Generate TypeScript code for a single step"""
127+
lines = []
128+
129+
if step.type == 'navigation':
130+
lines.append(f'{indent}// Navigate to {step.url}')
131+
lines.append(f'{indent}await browser.getPage().goto(\'{step.url}\');')
132+
lines.append(f'{indent}await browser.getPage().waitForLoadState(\'networkidle\');')
133+
134+
elif step.type == 'click':
135+
if step.selector:
136+
lines.append(f'{indent}// Click: {step.selector}')
137+
lines.append(f'{indent}const snap = await snapshot(browser);')
138+
lines.append(f'{indent}const element = find(snap, \'{step.selector}\');')
139+
lines.append(f'{indent}if (element) {{')
140+
lines.append(f'{indent} await click(browser, element.id);')
141+
lines.append(f'{indent}}} else {{')
142+
lines.append(f'{indent} throw new Error(\'Element not found: {step.selector}\');')
143+
lines.append(f'{indent}}}')
144+
elif step.element_id is not None:
145+
lines.append(f'{indent}// TODO: replace with semantic selector')
146+
lines.append(f'{indent}await click(browser, {step.element_id});')
147+
lines.append('')
148+
149+
elif step.type == 'type':
150+
if step.selector:
151+
lines.append(f'{indent}// Type into: {step.selector}')
152+
lines.append(f'{indent}const snap = await snapshot(browser);')
153+
lines.append(f'{indent}const element = find(snap, \'{step.selector}\');')
154+
lines.append(f'{indent}if (element) {{')
155+
lines.append(f'{indent} await typeText(browser, element.id, \'{step.text}\');')
156+
lines.append(f'{indent}}} else {{')
157+
lines.append(f'{indent} throw new Error(\'Element not found: {step.selector}\');')
158+
lines.append(f'{indent}}}')
159+
elif step.element_id is not None:
160+
lines.append(f'{indent}// TODO: replace with semantic selector')
161+
lines.append(f'{indent}await typeText(browser, {step.element_id}, \'{step.text}\');')
162+
lines.append('')
163+
164+
elif step.type == 'press':
165+
lines.append(f'{indent}// Press key: {step.key}')
166+
lines.append(f'{indent}await press(browser, \'{step.key}\');')
167+
lines.append('')
168+
169+
return lines
170+
171+
def save_python(self, filepath: str) -> None:
172+
"""Generate and save Python script"""
173+
code = self.generate_python()
174+
with open(filepath, 'w') as f:
175+
f.write(code)
176+
177+
def save_typescript(self, filepath: str) -> None:
178+
"""Generate and save TypeScript script"""
179+
code = self.generate_typescript()
180+
with open(filepath, 'w') as f:
181+
f.write(code)
182+
183+
184+
def generate(trace: Trace, language: str = 'py') -> str:
185+
"""
186+
Generate script from trace
187+
188+
Args:
189+
trace: Trace object
190+
language: 'py' or 'ts'
191+
192+
Returns:
193+
Generated code as string
194+
"""
195+
generator = ScriptGenerator(trace)
196+
if language == 'py':
197+
return generator.generate_python()
198+
elif language == 'ts':
199+
return generator.generate_typescript()
200+
else:
201+
raise ValueError(f"Unsupported language: {language}. Use 'py' or 'ts'")
202+

0 commit comments

Comments
 (0)