aetherius 0.2.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 (154) hide show
  1. aetherius/__init__.py +42 -0
  2. aetherius/__main__.py +12 -0
  3. aetherius/_contracts/__init__.py +0 -0
  4. aetherius/_contracts/blueprint.schema.json +144 -0
  5. aetherius/acts/__init__.py +1 -0
  6. aetherius/acts/_shared.py +61 -0
  7. aetherius/acts/continuum/__init__.py +7 -0
  8. aetherius/acts/continuum/actions.py +149 -0
  9. aetherius/acts/continuum/bridge.py +159 -0
  10. aetherius/acts/continuum/browser.py +164 -0
  11. aetherius/acts/continuum/debug_overlay.py +65 -0
  12. aetherius/acts/continuum/driver.py +127 -0
  13. aetherius/acts/continuum/human_actions.py +74 -0
  14. aetherius/acts/oracle/__init__.py +1 -0
  15. aetherius/acts/oracle/driver.py +1 -0
  16. aetherius/acts/oracle/locator.py +1 -0
  17. aetherius/acts/oracle/model.py +1 -0
  18. aetherius/acts/oracle/perception.py +1 -0
  19. aetherius/acts/phantom/__init__.py +1 -0
  20. aetherius/acts/phantom/driver.py +1 -0
  21. aetherius/acts/phantom/loop.py +1 -0
  22. aetherius/acts/phantom/memory.py +1 -0
  23. aetherius/acts/phantom/perception.py +1 -0
  24. aetherius/acts/phantom/planner.py +1 -0
  25. aetherius/acts/vector/__init__.py +1 -0
  26. aetherius/acts/vector/auth.py +108 -0
  27. aetherius/acts/vector/client.py +129 -0
  28. aetherius/acts/vector/driver.py +128 -0
  29. aetherius/builder/__init__.py +40 -0
  30. aetherius/builder/catalog.py +93 -0
  31. aetherius/builder/factory.py +243 -0
  32. aetherius/builder/templates.py +150 -0
  33. aetherius/builder/validation.py +120 -0
  34. aetherius/cli.py +236 -0
  35. aetherius/config/__init__.py +1 -0
  36. aetherius/config/secrets.py +71 -0
  37. aetherius/config/settings.py +44 -0
  38. aetherius/console/__init__.py +1 -0
  39. aetherius/console/app.py +66 -0
  40. aetherius/console/console.tcss +97 -0
  41. aetherius/console/daemon_control.py +83 -0
  42. aetherius/console/run_bridge.py +43 -0
  43. aetherius/console/screens/__init__.py +1 -0
  44. aetherius/console/screens/_pending.py +38 -0
  45. aetherius/console/screens/builder/__init__.py +1 -0
  46. aetherius/console/screens/builder/act_picker.py +75 -0
  47. aetherius/console/screens/builder/io_editor.py +167 -0
  48. aetherius/console/screens/builder/options_editor.py +132 -0
  49. aetherius/console/screens/builder/preview.py +57 -0
  50. aetherius/console/screens/builder/screen.py +235 -0
  51. aetherius/console/screens/builder/step_editor.py +249 -0
  52. aetherius/console/screens/catalog.py +52 -0
  53. aetherius/console/screens/home.py +90 -0
  54. aetherius/console/screens/library.py +124 -0
  55. aetherius/console/screens/library_scan.py +99 -0
  56. aetherius/console/screens/recorder.py +193 -0
  57. aetherius/console/screens/runs.py +138 -0
  58. aetherius/console/screens/sessions.py +14 -0
  59. aetherius/console/screens/settings.py +132 -0
  60. aetherius/console/screenshots.py +173 -0
  61. aetherius/console/theme.py +114 -0
  62. aetherius/console/widgets/__init__.py +1 -0
  63. aetherius/console/widgets/event_log.py +44 -0
  64. aetherius/console/widgets/form.py +101 -0
  65. aetherius/console/widgets/json_preview.py +18 -0
  66. aetherius/console/widgets/run_summary.py +82 -0
  67. aetherius/core/__init__.py +1 -0
  68. aetherius/core/actions/__init__.py +1 -0
  69. aetherius/core/actions/base.py +99 -0
  70. aetherius/core/actions/data.py +126 -0
  71. aetherius/core/actions/flow.py +76 -0
  72. aetherius/core/actions/interaction.py +109 -0
  73. aetherius/core/actions/navigation.py +32 -0
  74. aetherius/core/actions/registry.py +70 -0
  75. aetherius/core/actions/spec.py +42 -0
  76. aetherius/core/blueprint/__init__.py +1 -0
  77. aetherius/core/blueprint/loader.py +82 -0
  78. aetherius/core/blueprint/models.py +81 -0
  79. aetherius/core/blueprint/template.py +101 -0
  80. aetherius/core/blueprint/validator.py +45 -0
  81. aetherius/core/driver.py +36 -0
  82. aetherius/core/errors.py +134 -0
  83. aetherius/core/events/__init__.py +1 -0
  84. aetherius/core/events/bus.py +28 -0
  85. aetherius/core/events/models.py +29 -0
  86. aetherius/core/events/sinks.py +55 -0
  87. aetherius/core/extraction/__init__.py +1 -0
  88. aetherius/core/extraction/html_extractor.py +52 -0
  89. aetherius/core/extraction/json_extractor.py +125 -0
  90. aetherius/core/runtime/__init__.py +1 -0
  91. aetherius/core/runtime/context.py +61 -0
  92. aetherius/core/runtime/engine.py +188 -0
  93. aetherius/core/runtime/result.py +39 -0
  94. aetherius/core/runtime/selector.py +1 -0
  95. aetherius/models/__init__.py +1 -0
  96. aetherius/models/registry.py +1 -0
  97. aetherius/models/store/.gitkeep +0 -0
  98. aetherius/recorder/__init__.py +12 -0
  99. aetherius/recorder/_capture_js.py +80 -0
  100. aetherius/recorder/_gesture_js.py +40 -0
  101. aetherius/recorder/_names.py +20 -0
  102. aetherius/recorder/_overlay_js.py +262 -0
  103. aetherius/recorder/_playwright.py +49 -0
  104. aetherius/recorder/_selector_js.py +148 -0
  105. aetherius/recorder/_transform.py +214 -0
  106. aetherius/recorder/_vector_js.py +174 -0
  107. aetherius/recorder/base.py +92 -0
  108. aetherius/recorder/blueprint_recorder.py +72 -0
  109. aetherius/recorder/capture.py +59 -0
  110. aetherius/recorder/continuum_backend.py +119 -0
  111. aetherius/recorder/gesture_recorder.py +193 -0
  112. aetherius/recorder/selector_synth.py +83 -0
  113. aetherius/recorder/session.py +101 -0
  114. aetherius/recorder/vector_backend.py +158 -0
  115. aetherius/server/__init__.py +12 -0
  116. aetherius/server/app.py +37 -0
  117. aetherius/server/config.py +25 -0
  118. aetherius/server/deps.py +50 -0
  119. aetherius/server/jobs.py +155 -0
  120. aetherius/server/routes/__init__.py +1 -0
  121. aetherius/server/routes/blueprints.py +45 -0
  122. aetherius/server/routes/recorder.py +25 -0
  123. aetherius/server/routes/runs.py +49 -0
  124. aetherius/server/routes/stream.py +53 -0
  125. aetherius/server/schemas.py +78 -0
  126. aetherius/stealth/__init__.py +1 -0
  127. aetherius/stealth/fingerprint/__init__.py +1 -0
  128. aetherius/stealth/fingerprint/patch.py +38 -0
  129. aetherius/stealth/fingerprint/profile.py +95 -0
  130. aetherius/stealth/gestures/__init__.py +1 -0
  131. aetherius/stealth/gestures/data/human_library.json +7107 -0
  132. aetherius/stealth/gestures/library.py +126 -0
  133. aetherius/stealth/gestures/seed.py +97 -0
  134. aetherius/stealth/humanizer/__init__.py +1 -0
  135. aetherius/stealth/humanizer/input.py +89 -0
  136. aetherius/stealth/humanizer/keyboard.py +75 -0
  137. aetherius/stealth/humanizer/mouse.py +153 -0
  138. aetherius/stealth/humanizer/scroll.py +55 -0
  139. aetherius/stealth/humanizer/timing.py +54 -0
  140. aetherius/stealth/ml/__init__.py +1 -0
  141. aetherius/stealth/ml/fingerprint_model.py +1 -0
  142. aetherius/stealth/ml/motion_model.py +1 -0
  143. aetherius/stealth/policy.py +133 -0
  144. aetherius/stealth/session/__init__.py +1 -0
  145. aetherius/stealth/session/store.py +35 -0
  146. aetherius/stealth/session/warmup.py +52 -0
  147. aetherius/version.py +3 -0
  148. aetherius-0.2.0.data/data/aetherius/_contracts/__init__.py +0 -0
  149. aetherius-0.2.0.data/data/aetherius/_contracts/blueprint.schema.json +144 -0
  150. aetherius-0.2.0.dist-info/METADATA +606 -0
  151. aetherius-0.2.0.dist-info/RECORD +154 -0
  152. aetherius-0.2.0.dist-info/WHEEL +4 -0
  153. aetherius-0.2.0.dist-info/entry_points.txt +2 -0
  154. aetherius-0.2.0.dist-info/licenses/LICENSE +11 -0
@@ -0,0 +1,125 @@
1
+ """JSON extraction via JSONPath, with optional per-item field mapping and a safe where predicate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import json
7
+ import types
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from jsonpath_ng.ext import parse as jpath_parse
12
+
13
+ from ..errors import ExtractionError
14
+
15
+ # AST node types allowed in a `where` expression.
16
+ # Anything outside this set (calls, imports, comprehensions, …) is rejected.
17
+ _ALLOWED_NODES = (
18
+ ast.Expression,
19
+ ast.Compare,
20
+ ast.Name,
21
+ ast.Attribute,
22
+ ast.Constant,
23
+ ast.BoolOp,
24
+ ast.And,
25
+ ast.Or,
26
+ ast.UnaryOp,
27
+ ast.Not,
28
+ ast.Eq,
29
+ ast.NotEq,
30
+ ast.Lt,
31
+ ast.LtE,
32
+ ast.Gt,
33
+ ast.GtE,
34
+ ast.In,
35
+ ast.NotIn,
36
+ ast.Is,
37
+ ast.IsNot,
38
+ ast.Load,
39
+ )
40
+
41
+
42
+ @dataclass
43
+ class ExtractSpec:
44
+ from_: str # "json" | "html"
45
+ path: str # JSONPath expression, e.g. "$[*]"
46
+ where: str | None = None # per-item predicate, e.g. "item.eventCategory != 'Vacances'"
47
+ fields: dict[str, str] = field(default_factory=dict) # name -> JSONPath relative to item
48
+
49
+
50
+ def _eval_predicate(expr: str, item: Any) -> bool:
51
+ """Evaluate a simple comparison expression against *item* with an AST safety check.
52
+
53
+ *item* is wrapped in SimpleNamespace so `item.attr` dot-access works on JSON dicts.
54
+ Raises ExtractionError if the expression contains disallowed AST nodes.
55
+ """
56
+ try:
57
+ tree = ast.parse(expr, mode="eval")
58
+ except SyntaxError as exc:
59
+ raise ExtractionError(f"Invalid where expression {expr!r}: {exc}") from exc
60
+
61
+ for node in ast.walk(tree):
62
+ if not isinstance(node, _ALLOWED_NODES):
63
+ raise ExtractionError(
64
+ f"Disallowed construct {type(node).__name__!r} in where expression {expr!r}. "
65
+ "Only comparisons and boolean logic are permitted."
66
+ )
67
+
68
+ ns: Any
69
+ if isinstance(item, dict):
70
+ ns = types.SimpleNamespace(**item)
71
+ else:
72
+ ns = item
73
+
74
+ try:
75
+ return bool(eval(compile(tree, "<where>", "eval"), {"__builtins__": {}}, {"item": ns}))
76
+ except Exception as exc:
77
+ raise ExtractionError(f"Error evaluating where expression {expr!r}: {exc}") from exc
78
+
79
+
80
+ def _extract_fields(item_dict: dict[str, Any], fields: dict[str, str]) -> dict[str, Any]:
81
+ """Extract sub-fields from a single matched item using relative JSONPath expressions."""
82
+ result: dict[str, Any] = {}
83
+ for field_name, path in fields.items():
84
+ matches = jpath_parse(path).find(item_dict)
85
+ if not matches:
86
+ result[field_name] = None
87
+ elif len(matches) == 1:
88
+ result[field_name] = matches[0].value
89
+ else:
90
+ result[field_name] = [m.value for m in matches]
91
+ return result
92
+
93
+
94
+ def extract_json(body: bytes, spec: dict[str, "ExtractSpec"]) -> dict[str, Any]:
95
+ """Run all extraction specs in *spec* against a JSON *body* and return the collected data.
96
+
97
+ Each key in *spec* becomes a key in the returned dict whose value is a list of matched items
98
+ (or a single value when only one item matches and no fields mapping is defined).
99
+ """
100
+ try:
101
+ data = json.loads(body)
102
+ except json.JSONDecodeError as exc:
103
+ raise ExtractionError(f"Cannot parse JSON response body: {exc}") from exc
104
+
105
+ result: dict[str, Any] = {}
106
+
107
+ for output_name, s in spec.items():
108
+ try:
109
+ matches = jpath_parse(s.path).find(data)
110
+ except Exception as exc:
111
+ raise ExtractionError(f"Invalid JSONPath {s.path!r}: {exc}") from exc
112
+
113
+ items = [m.value for m in matches]
114
+
115
+ if s.where:
116
+ items = [item for item in items if _eval_predicate(s.where, item)]
117
+
118
+ if s.fields:
119
+ items = [
120
+ _extract_fields(item if isinstance(item, dict) else {}, s.fields) for item in items
121
+ ]
122
+
123
+ result[output_name] = items
124
+
125
+ return result
@@ -0,0 +1 @@
1
+ """Runtime: the orchestrator that turns a Blueprint into a Result by driving the selected Act."""
@@ -0,0 +1,61 @@
1
+ """RunContext: mutable per-run state (resolved inputs, injected secrets, step outputs)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Mapping
9
+
10
+ from ..blueprint.models import Blueprint
11
+ from ..errors import BlueprintValidationError
12
+
13
+
14
+ @dataclass
15
+ class RunContext:
16
+ run_id: str
17
+ blueprint: Blueprint
18
+ inputs: dict[str, Any]
19
+ secrets: dict[str, str]
20
+ step_outputs: dict[str, Any] = field(default_factory=dict)
21
+ started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
22
+
23
+ def template_ctx(self) -> dict[str, Any]:
24
+ """Build the Jinja2 context dict for rendering step fields and outputs."""
25
+ return {
26
+ "inputs": self.inputs,
27
+ "secrets": self.secrets,
28
+ "vars": self.blueprint.vars,
29
+ "env": dict(os.environ),
30
+ "steps": self.step_outputs,
31
+ }
32
+
33
+
34
+ def resolve_inputs(
35
+ blueprint: Blueprint,
36
+ raw: Mapping[str, Any] | None,
37
+ ) -> dict[str, Any]:
38
+ """Validate and resolve caller-supplied inputs against the Blueprint input specs.
39
+
40
+ Raises:
41
+ BlueprintValidationError: a required input is missing.
42
+ """
43
+ raw = dict(raw or {})
44
+ resolved: dict[str, Any] = {}
45
+
46
+ for name, spec in blueprint.inputs.items():
47
+ if name in raw:
48
+ resolved[name] = raw[name]
49
+ elif spec.default is not None:
50
+ resolved[name] = spec.default
51
+ elif spec.required:
52
+ raise BlueprintValidationError(
53
+ f"Missing required input '{name}' for Blueprint '{blueprint.name}'."
54
+ )
55
+
56
+ # Pass through any extra caller inputs (Blueprint may not declare all runtime vars).
57
+ for name, value in raw.items():
58
+ if name not in resolved:
59
+ resolved[name] = value
60
+
61
+ return resolved
@@ -0,0 +1,188 @@
1
+ """RunEngine: orchestrates Act selection, step pipeline, retries, events, and Result assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Mapping
9
+
10
+ from ..blueprint.models import Blueprint
11
+ from ..blueprint.template import render_value
12
+ from ..blueprint.validator import validate_for_act
13
+ from ..errors import AetheriusError, ActionError, RunError
14
+ from ..events.bus import EventBus
15
+ from ..events.models import EventType, RunEvent
16
+ from ..events.sinks import LogSink, NullSink, Sink
17
+ from ..runtime.context import RunContext, resolve_inputs
18
+ from ..runtime.result import Result, RunStatus, StepResult
19
+
20
+
21
+ IMPLEMENTED_ACTS: frozenset[str] = frozenset({"vector", "continuum"})
22
+
23
+
24
+ def _make_driver(act: str) -> Any:
25
+ # Drivers are imported lazily so `import aetherius` never pulls in an Act's heavy
26
+ # dependencies (Playwright, ONNX, ...). Each driver defers its own extra to runtime.
27
+ if act == "vector":
28
+ from ...acts.vector.driver import VectorDriver
29
+
30
+ return VectorDriver()
31
+ if act == "continuum":
32
+ from ...acts.continuum.driver import ContinuumDriver
33
+
34
+ return ContinuumDriver()
35
+ raise ActionError(f"Act {act!r} is not implemented yet. Available: {sorted(IMPLEMENTED_ACTS)}.")
36
+
37
+
38
+ class RunEngine:
39
+ def run(
40
+ self,
41
+ blueprint: Blueprint,
42
+ inputs: Mapping[str, Any] | None = None,
43
+ secrets: Mapping[str, str] | None = None,
44
+ *,
45
+ sinks: list[Sink] | None = None,
46
+ run_id: str | None = None,
47
+ ) -> Result:
48
+ validate_for_act(blueprint)
49
+
50
+ # The daemon assigns the id it returned to the caller (HTTP 202) before the run starts, so
51
+ # the streamed events carry a run_id the client already holds. In-process callers omit it.
52
+ run_id = run_id or uuid.uuid4().hex
53
+ started_at = datetime.now(timezone.utc)
54
+
55
+ from ...config.secrets import resolve_secrets
56
+
57
+ resolved_inputs = resolve_inputs(blueprint, inputs)
58
+ resolved_secrets = resolve_secrets(blueprint.secrets, secrets)
59
+ ctx = RunContext(
60
+ run_id=run_id,
61
+ blueprint=blueprint,
62
+ inputs=resolved_inputs,
63
+ secrets=resolved_secrets,
64
+ started_at=started_at,
65
+ )
66
+
67
+ bus = EventBus()
68
+ if sinks is not None:
69
+ for sink in sinks:
70
+ bus.register(sink)
71
+ else:
72
+ default_sink: Sink = (
73
+ LogSink(debug=blueprint.options.debug) if blueprint.options.debug else NullSink()
74
+ )
75
+ bus.register(default_sink)
76
+
77
+ driver = _make_driver(blueprint.act)
78
+ driver.setup(ctx)
79
+
80
+ bus.emit(
81
+ RunEvent(
82
+ run_id=run_id,
83
+ type=EventType.PROGRESS,
84
+ message=f"run started: {blueprint.name}",
85
+ level="info",
86
+ )
87
+ )
88
+
89
+ step_results: list[StepResult] = []
90
+ final_status = RunStatus.SUCCESS
91
+ run_error: str | None = None
92
+
93
+ try:
94
+ for i, step in enumerate(blueprint.steps):
95
+ step_key = step.id or f"_step_{i}"
96
+
97
+ bus.emit(
98
+ RunEvent(
99
+ run_id=run_id, type=EventType.STEP_STARTED, step_id=step.id, level="debug"
100
+ )
101
+ )
102
+ t0 = time.monotonic()
103
+
104
+ def renderer(value: Any) -> Any:
105
+ return render_value(value, ctx.template_ctx())
106
+
107
+ try:
108
+ outputs = driver.run_step(step, ctx, bus, renderer)
109
+ ctx.step_outputs[step_key] = outputs
110
+ duration_ms = (time.monotonic() - t0) * 1000
111
+ step_results.append(
112
+ StepResult(
113
+ step_id=step.id,
114
+ action=step.action,
115
+ status=RunStatus.SUCCESS,
116
+ outputs=outputs,
117
+ duration_ms=duration_ms,
118
+ )
119
+ )
120
+ bus.emit(
121
+ RunEvent(
122
+ run_id=run_id,
123
+ type=EventType.STEP_FINISHED,
124
+ step_id=step.id,
125
+ level="debug",
126
+ )
127
+ )
128
+
129
+ except AetheriusError as exc:
130
+ duration_ms = (time.monotonic() - t0) * 1000
131
+ step_results.append(
132
+ StepResult(
133
+ step_id=step.id,
134
+ action=step.action,
135
+ status=RunStatus.FAILED,
136
+ error=str(exc),
137
+ duration_ms=duration_ms,
138
+ )
139
+ )
140
+ bus.emit(
141
+ RunEvent(
142
+ run_id=run_id,
143
+ type=EventType.ERROR,
144
+ step_id=step.id,
145
+ message=str(exc),
146
+ level="error",
147
+ )
148
+ )
149
+ raise
150
+
151
+ except AetheriusError as exc:
152
+ final_status = RunStatus.FAILED
153
+ run_error = str(exc)
154
+
155
+ except Exception as exc:
156
+ final_status = RunStatus.FAILED
157
+ run_error = str(exc)
158
+ raise RunError(f"Unexpected error during run {run_id}: {exc}", cause=exc) from exc
159
+
160
+ finally:
161
+ driver.teardown(ctx)
162
+
163
+ # Render the outputs dict through the template engine.
164
+ final_outputs: dict[str, Any] = {}
165
+ if final_status == RunStatus.SUCCESS and blueprint.outputs:
166
+ final_outputs = render_value(blueprint.outputs, ctx.template_ctx())
167
+
168
+ finished_at = datetime.now(timezone.utc)
169
+ bus.emit(
170
+ RunEvent(
171
+ run_id=run_id,
172
+ type=EventType.DONE,
173
+ message=f"run finished: {final_status.value}",
174
+ level="info" if final_status == RunStatus.SUCCESS else "error",
175
+ data={"status": final_status.value, "error": run_error},
176
+ )
177
+ )
178
+
179
+ return Result(
180
+ run_id=run_id,
181
+ blueprint_name=blueprint.name,
182
+ status=final_status,
183
+ outputs=final_outputs,
184
+ step_results=step_results,
185
+ error=run_error,
186
+ started_at=started_at,
187
+ finished_at=finished_at,
188
+ )
@@ -0,0 +1,39 @@
1
+ """Result and StepResult: structured outputs returned to the caller after a Blueprint run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class RunStatus(str, Enum):
13
+ SUCCESS = "success"
14
+ FAILED = "failed"
15
+ PARTIAL = "partial"
16
+
17
+
18
+ class StepResult(BaseModel):
19
+ step_id: str | None
20
+ action: str
21
+ status: RunStatus
22
+ outputs: dict[str, Any] = Field(default_factory=dict)
23
+ error: str | None = None
24
+ duration_ms: float
25
+
26
+
27
+ class Result(BaseModel):
28
+ run_id: str
29
+ blueprint_name: str
30
+ status: RunStatus
31
+ outputs: dict[str, Any] = Field(default_factory=dict)
32
+ step_results: list[StepResult] = Field(default_factory=list)
33
+ error: str | None = None
34
+ started_at: datetime
35
+ finished_at: datetime
36
+
37
+ @property
38
+ def duration_ms(self) -> float:
39
+ return (self.finished_at - self.started_at).total_seconds() * 1000
@@ -0,0 +1 @@
1
+ """Unified target model abstracting css, xpath, text and vision targets so actions are expressed independently of the Act."""
@@ -0,0 +1 @@
1
+ """Runtime ML asset management (download, cache, versioning). Training lives under training/."""
@@ -0,0 +1 @@
1
+ """Model registry: resolve, download, cache and version the vision models referenced by Blueprints."""
File without changes
@@ -0,0 +1,12 @@
1
+ """Recorders that generate artifacts: Blueprints from a live session, and mouse gestures for the stealth library.
2
+
3
+ Public entry points are re-exported here. None of the imports pull Playwright at module load: the
4
+ browser is imported lazily inside the recording functions, so ``import aetherius`` stays light.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .blueprint_recorder import record_blueprint
10
+ from .gesture_recorder import record_gestures
11
+
12
+ __all__ = ["record_blueprint", "record_gestures"]
@@ -0,0 +1,80 @@
1
+ """In-page capture script for the Blueprint recorder (action capture).
2
+
3
+ Listens for the real DOM events a demonstration produces (click, change, Enter) and reports a JSON
4
+ descriptor for each to the ``__aetherius_capture`` binding. Element descriptors are built by the
5
+ shared primitives on ``window.__aeSel`` (:mod:`._selector_js`), so the candidate/uniqueness logic is
6
+ defined once and shared with the pick overlay.
7
+
8
+ Two guards keep action capture and the overlay from stepping on each other: events originating in the
9
+ overlay are ignored, and while a pick mode is active (``window.__aeRecorderPicking``) page actions are
10
+ not recorded — the user is designating data, not driving the site.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ # Self-invoking, guarded against double injection. Password fields never send their value: only a
16
+ # redacted marker. Requires window.__aeSel (inject _selector_js.SELECTOR_JS first).
17
+ RECORDER_JS = r"""
18
+ (() => {
19
+ if (window.__aetheriusRecorder) return;
20
+ window.__aetheriusRecorder = true;
21
+
22
+ const TEXTUAL = new Set(['text', 'email', 'search', 'tel', 'url', 'number', 'password', '']);
23
+ const OVERLAY_ID = '__ae-recorder-overlay';
24
+
25
+ const send = (payload) => {
26
+ try { window.__aetherius_capture(JSON.stringify(payload)); } catch (e) {}
27
+ };
28
+
29
+ // Ignore events from the overlay itself, and any event while a pick mode is active.
30
+ const ignored = (e) => {
31
+ if (window.__aeRecorderPicking) return true;
32
+ const path = e.composedPath ? e.composedPath() : [];
33
+ return path.some((n) => n && n.id === OVERLAY_ID);
34
+ };
35
+
36
+ document.addEventListener('click', (e) => {
37
+ if (ignored(e)) return;
38
+ const el = e.target.closest(window.__aeSel.CLICKABLE);
39
+ if (!el) return; // plain clicks (focusing a text field, selecting text) are not steps
40
+ const payload = { kind: 'click', descriptor: window.__aeSel.descriptorFor(el) };
41
+ // A link navigating to a real URL is far more robust replayed as a navigate (stable URL) than as
42
+ // a click (fragile selector). Buttons/submits have no reproducible URL, so they stay clicks.
43
+ if (el.tagName === 'A') {
44
+ const raw = el.getAttribute('href') || '';
45
+ if (raw && !raw.startsWith('#') && !raw.startsWith('javascript:') && /^https?:/i.test(el.href)) {
46
+ payload.href = el.href;
47
+ }
48
+ }
49
+ send(payload);
50
+ }, true);
51
+
52
+ document.addEventListener('change', (e) => {
53
+ if (ignored(e)) return;
54
+ const el = e.target;
55
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
56
+ if (tag === 'select') {
57
+ send({ kind: 'select', value: el.value, descriptor: window.__aeSel.descriptorFor(el) });
58
+ return;
59
+ }
60
+ if (tag !== 'input' && tag !== 'textarea') return;
61
+ const type = (el.getAttribute('type') || 'text').toLowerCase();
62
+ if (type === 'checkbox' || type === 'radio') return; // captured as a click
63
+ if (tag === 'input' && !TEXTUAL.has(type)) return;
64
+ if (type === 'password') {
65
+ send({ kind: 'fill', redacted: true, descriptor: window.__aeSel.descriptorFor(el) });
66
+ } else {
67
+ send({ kind: 'fill', value: el.value, descriptor: window.__aeSel.descriptorFor(el) });
68
+ }
69
+ }, true);
70
+
71
+ document.addEventListener('keydown', (e) => {
72
+ if (ignored(e)) return;
73
+ if (e.key !== 'Enter') return;
74
+ const el = e.target;
75
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
76
+ if (tag !== 'input') return; // Enter in a text field commonly submits without a visible button
77
+ send({ kind: 'press', key: 'Enter', descriptor: window.__aeSel.descriptorFor(el) });
78
+ }, true);
79
+ })();
80
+ """
@@ -0,0 +1,40 @@
1
+ """In-page capture script for the gesture recorder.
2
+
3
+ Streams raw pointer samples ``[x, y, t]`` (viewport pixels, seconds since page load) and click
4
+ timestamps to the ``__aetherius_gesture`` binding. Moves are buffered and flushed on a timer and on
5
+ every click, rather than sent one per ``pointermove``, so the binding is not hammered thousands of
6
+ times a second. Segmentation into individual gestures is done in pure Python (:mod:`.gesture_recorder`).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # Self-invoking, guarded against double injection. performance.now() is milliseconds; we send seconds.
12
+ GESTURE_RECORDER_JS = r"""
13
+ (() => {
14
+ if (window.__aetheriusGestureRecorder) return;
15
+ window.__aetheriusGestureRecorder = true;
16
+
17
+ let buffer = [];
18
+ let clickAt = null;
19
+
20
+ const flush = () => {
21
+ if (buffer.length === 0 && clickAt === null) return;
22
+ const payload = { moves: buffer, click: clickAt };
23
+ buffer = [];
24
+ clickAt = null;
25
+ try { window.__aetherius_gesture(JSON.stringify(payload)); } catch (e) {}
26
+ };
27
+
28
+ document.addEventListener('pointermove', (e) => {
29
+ buffer.push([Math.round(e.clientX), Math.round(e.clientY), +(performance.now() / 1000).toFixed(4)]);
30
+ }, true);
31
+
32
+ document.addEventListener('pointerdown', (e) => {
33
+ buffer.push([Math.round(e.clientX), Math.round(e.clientY), +(performance.now() / 1000).toFixed(4)]);
34
+ clickAt = +(performance.now() / 1000).toFixed(4);
35
+ flush();
36
+ }, true);
37
+
38
+ setInterval(flush, 250);
39
+ })();
40
+ """
@@ -0,0 +1,20 @@
1
+ """Tiny naming helpers shared by the recorder transforms: sanitize and de-duplicate identifiers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ def ident(raw: str | None, *, fallback: str) -> str:
9
+ """Sanitize a free string into a lowercase identifier, or *fallback* if it is empty."""
10
+ base = re.sub(r"[^a-z0-9_]+", "_", (raw or "").lower()).strip("_")
11
+ return base or fallback
12
+
13
+
14
+ def unique(base: str, taken: set[str]) -> str:
15
+ """Return *base*, suffixed (_2, _3, …) until it is not already in *taken*."""
16
+ name, counter = base, 2
17
+ while name in taken:
18
+ name = f"{base}_{counter}"
19
+ counter += 1
20
+ return name