seedcode-cli 6.1.5__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.
- seedcode/__init__.py +14 -0
- seedcode/__main__.py +12 -0
- seedcode/app.py +508 -0
- seedcode/apps/__init__.py +32 -0
- seedcode/apps/discovery.py +241 -0
- seedcode/apps/installer.py +164 -0
- seedcode/apps/launcher.py +156 -0
- seedcode/apps/verifier.py +119 -0
- seedcode/assets/logo.txt +15 -0
- seedcode/cli.py +95 -0
- seedcode/commands/__init__.py +81 -0
- seedcode/commands/about.py +34 -0
- seedcode/commands/agent.py +94 -0
- seedcode/commands/assist.py +201 -0
- seedcode/commands/clear.py +20 -0
- seedcode/commands/desktop.py +104 -0
- seedcode/commands/doctor.py +152 -0
- seedcode/commands/help.py +61 -0
- seedcode/commands/history.py +365 -0
- seedcode/commands/palette.py +100 -0
- seedcode/commands/provider.py +451 -0
- seedcode/commands/theme.py +76 -0
- seedcode/computer/__init__.py +98 -0
- seedcode/computer/browser.py +276 -0
- seedcode/computer/browser_cdp.py +567 -0
- seedcode/computer/browser_engine.py +546 -0
- seedcode/computer/browser_extract.py +301 -0
- seedcode/computer/browser_popups.py +329 -0
- seedcode/computer/browser_selenium.py +209 -0
- seedcode/computer/browser_skills.py +245 -0
- seedcode/computer/catalog.py +200 -0
- seedcode/computer/controller.py +324 -0
- seedcode/computer/dispatcher.py +272 -0
- seedcode/computer/dpi.py +185 -0
- seedcode/computer/engine.py +105 -0
- seedcode/computer/keyboard.py +101 -0
- seedcode/computer/logbook.py +104 -0
- seedcode/computer/mouse.py +48 -0
- seedcode/computer/ocr.py +213 -0
- seedcode/computer/operator_skills.py +577 -0
- seedcode/computer/permissions.py +203 -0
- seedcode/computer/recovery.py +115 -0
- seedcode/computer/registry.py +107 -0
- seedcode/computer/resolver.py +434 -0
- seedcode/computer/screen.py +130 -0
- seedcode/computer/screen_state.py +412 -0
- seedcode/computer/selfguard.py +197 -0
- seedcode/computer/semantic.py +100 -0
- seedcode/computer/skills.py +139 -0
- seedcode/computer/state.py +199 -0
- seedcode/computer/verifier.py +177 -0
- seedcode/computer/vision.py +327 -0
- seedcode/computer/windows.py +217 -0
- seedcode/config/__init__.py +8 -0
- seedcode/config/defaults.py +22 -0
- seedcode/config/manager.py +62 -0
- seedcode/core/__init__.py +31 -0
- seedcode/core/agent.py +534 -0
- seedcode/core/chat.py +128 -0
- seedcode/core/client.py +9 -0
- seedcode/core/errors.py +199 -0
- seedcode/core/identity.py +66 -0
- seedcode/core/identity_store.py +119 -0
- seedcode/core/lifecycle.py +240 -0
- seedcode/core/limits.py +35 -0
- seedcode/core/models.py +347 -0
- seedcode/core/project.py +96 -0
- seedcode/core/providers/__init__.py +58 -0
- seedcode/core/providers/aerolink.py +324 -0
- seedcode/core/providers/base.py +230 -0
- seedcode/core/providers/freemodel.py +931 -0
- seedcode/core/providers/ollama.py +262 -0
- seedcode/core/providers/openrouter.py +393 -0
- seedcode/core/streaming.py +21 -0
- seedcode/memory/__init__.py +8 -0
- seedcode/memory/manager.py +47 -0
- seedcode/memory/storage.py +38 -0
- seedcode/memory/store.py +257 -0
- seedcode/tools/__init__.py +35 -0
- seedcode/tools/base.py +179 -0
- seedcode/tools/desktop.py +371 -0
- seedcode/tools/filesystem.py +309 -0
- seedcode/tools/git.py +72 -0
- seedcode/tools/patch.py +170 -0
- seedcode/tools/permissions.py +288 -0
- seedcode/tools/search.py +137 -0
- seedcode/tools/terminal.py +200 -0
- seedcode/tools/textio.py +59 -0
- seedcode/ui/__init__.py +164 -0
- seedcode/ui/badges.py +64 -0
- seedcode/ui/banner.py +78 -0
- seedcode/ui/dashboard.py +197 -0
- seedcode/ui/dialog.py +62 -0
- seedcode/ui/fuzzy.py +128 -0
- seedcode/ui/layout.py +54 -0
- seedcode/ui/menu.py +61 -0
- seedcode/ui/palette.py +40 -0
- seedcode/ui/progress.py +41 -0
- seedcode/ui/prompts.py +16 -0
- seedcode/ui/renderer.py +36 -0
- seedcode/ui/searchbox.py +70 -0
- seedcode/ui/selector.py +514 -0
- seedcode/ui/statusbar.py +38 -0
- seedcode/ui/textbox.py +61 -0
- seedcode/ui/theme.py +204 -0
- seedcode/ui/tree.py +91 -0
- seedcode/utils/__init__.py +22 -0
- seedcode/utils/helpers.py +97 -0
- seedcode/utils/logger.py +65 -0
- seedcode_cli-6.1.5.dist-info/METADATA +368 -0
- seedcode_cli-6.1.5.dist-info/RECORD +114 -0
- seedcode_cli-6.1.5.dist-info/WHEEL +4 -0
- seedcode_cli-6.1.5.dist-info/entry_points.txt +2 -0
- seedcode_cli-6.1.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Optional Selenium fallback for deep, DOM-level browser automation.
|
|
2
|
+
|
|
3
|
+
This is NOT the primary browser mechanism — :mod:`seedcode.computer.browser`
|
|
4
|
+
drives the user's real default browser through the OS and the UI resolver, with
|
|
5
|
+
no driver downloads and full offline support. Selenium is only used when a task
|
|
6
|
+
genuinely needs DOM-level control (CSS/XPath selectors, ``execute_script``) and
|
|
7
|
+
the user has installed the optional extra.
|
|
8
|
+
|
|
9
|
+
Kept isolated here so importing the Computer Engine never pulls in Selenium, and
|
|
10
|
+
so the default-browser path has zero dependency on WebDriver being present.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from selenium import webdriver
|
|
20
|
+
|
|
21
|
+
_driver: "webdriver.Chrome | webdriver.Edge | None" = None
|
|
22
|
+
_browser_type: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BrowserError(Exception):
|
|
26
|
+
"""Selenium-backed browser automation error."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_available() -> tuple[bool, str]:
|
|
30
|
+
"""Whether the optional Selenium fallback can run."""
|
|
31
|
+
try:
|
|
32
|
+
import selenium # noqa: F401
|
|
33
|
+
|
|
34
|
+
return True, ""
|
|
35
|
+
except ImportError:
|
|
36
|
+
return False, "selenium not installed (pip install seedcode-cli[browser])"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_driver() -> "webdriver.Chrome | webdriver.Edge":
|
|
40
|
+
"""Get or create the WebDriver session."""
|
|
41
|
+
global _driver, _browser_type
|
|
42
|
+
|
|
43
|
+
if _driver is not None:
|
|
44
|
+
try:
|
|
45
|
+
_driver.current_url # liveness probe
|
|
46
|
+
return _driver
|
|
47
|
+
except Exception:
|
|
48
|
+
_driver = None
|
|
49
|
+
|
|
50
|
+
if _driver is None:
|
|
51
|
+
_driver, _browser_type = _launch_browser()
|
|
52
|
+
return _driver
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _launch_browser() -> tuple["webdriver.Chrome | webdriver.Edge", str]:
|
|
56
|
+
"""Launch Chrome or Edge under WebDriver."""
|
|
57
|
+
from selenium import webdriver
|
|
58
|
+
from selenium.webdriver.chrome.options import Options as ChromeOptions
|
|
59
|
+
from selenium.webdriver.chrome.service import Service as ChromeService
|
|
60
|
+
from selenium.webdriver.edge.options import Options as EdgeOptions
|
|
61
|
+
from selenium.webdriver.edge.service import Service as EdgeService
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
from webdriver_manager.chrome import ChromeDriverManager
|
|
65
|
+
from webdriver_manager.microsoft import EdgeChromiumDriverManager
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
options = ChromeOptions()
|
|
69
|
+
options.add_argument("--start-maximized")
|
|
70
|
+
options.add_experimental_option("excludeSwitches", ["enable-logging"])
|
|
71
|
+
service = ChromeService(ChromeDriverManager().install())
|
|
72
|
+
return webdriver.Chrome(service=service, options=options), "chrome"
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
options = EdgeOptions()
|
|
78
|
+
options.add_argument("--start-maximized")
|
|
79
|
+
options.add_experimental_option("excludeSwitches", ["enable-logging"])
|
|
80
|
+
service = EdgeService(EdgeChromiumDriverManager().install())
|
|
81
|
+
return webdriver.Edge(service=service, options=options), "edge"
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
except ImportError:
|
|
85
|
+
try:
|
|
86
|
+
options = ChromeOptions()
|
|
87
|
+
options.add_argument("--start-maximized")
|
|
88
|
+
return webdriver.Chrome(options=options), "chrome"
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
try:
|
|
92
|
+
options = EdgeOptions()
|
|
93
|
+
options.add_argument("--start-maximized")
|
|
94
|
+
return webdriver.Edge(options=options), "edge"
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
raise BrowserError(
|
|
99
|
+
"Could not launch a WebDriver browser. Install the optional extra: "
|
|
100
|
+
"pip install seedcode-cli[browser]"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def navigate(url: str) -> str:
|
|
105
|
+
driver = _get_driver()
|
|
106
|
+
if not url.startswith(("http://", "https://", "file://", "about:")):
|
|
107
|
+
url = "https://" + url
|
|
108
|
+
driver.get(url)
|
|
109
|
+
time.sleep(1)
|
|
110
|
+
return f"Navigated to: {driver.current_url}\nTitle: {driver.title}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def click_element(selector: str, selector_type: str = "css") -> str:
|
|
114
|
+
from selenium.webdriver.common.by import By
|
|
115
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
116
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
117
|
+
|
|
118
|
+
driver = _get_driver()
|
|
119
|
+
by_map = {
|
|
120
|
+
"css": By.CSS_SELECTOR, "xpath": By.XPATH, "id": By.ID, "name": By.NAME,
|
|
121
|
+
"class": By.CLASS_NAME, "tag": By.TAG_NAME, "link_text": By.LINK_TEXT,
|
|
122
|
+
"partial_link": By.PARTIAL_LINK_TEXT,
|
|
123
|
+
}
|
|
124
|
+
by = by_map.get(selector_type)
|
|
125
|
+
if by is None:
|
|
126
|
+
raise BrowserError(f"Unknown selector type: {selector_type}")
|
|
127
|
+
try:
|
|
128
|
+
element = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((by, selector)))
|
|
129
|
+
element.click()
|
|
130
|
+
return f"Clicked element: {selector}"
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
raise BrowserError(f"Could not click element '{selector}': {exc}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def type_in_element(selector: str, text: str, selector_type: str = "css", clear: bool = True) -> str:
|
|
136
|
+
from selenium.webdriver.common.by import By
|
|
137
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
138
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
139
|
+
|
|
140
|
+
driver = _get_driver()
|
|
141
|
+
by_map = {"css": By.CSS_SELECTOR, "xpath": By.XPATH, "id": By.ID, "name": By.NAME}
|
|
142
|
+
by = by_map.get(selector_type)
|
|
143
|
+
if by is None:
|
|
144
|
+
raise BrowserError(f"Unknown selector type: {selector_type}")
|
|
145
|
+
try:
|
|
146
|
+
element = WebDriverWait(driver, 10).until(EC.presence_of_element_located((by, selector)))
|
|
147
|
+
if clear:
|
|
148
|
+
element.clear()
|
|
149
|
+
element.send_keys(text)
|
|
150
|
+
return f"Typed into {selector}: {text}"
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
raise BrowserError(f"Could not type into '{selector}': {exc}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_page_info() -> str:
|
|
156
|
+
driver = _get_driver()
|
|
157
|
+
return f"URL: {driver.current_url}\nTitle: {driver.title}"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def find_elements(selector: str, selector_type: str = "css") -> str:
|
|
161
|
+
from selenium.webdriver.common.by import By
|
|
162
|
+
|
|
163
|
+
driver = _get_driver()
|
|
164
|
+
by_map = {
|
|
165
|
+
"css": By.CSS_SELECTOR, "xpath": By.XPATH, "id": By.ID, "name": By.NAME,
|
|
166
|
+
"class": By.CLASS_NAME, "tag": By.TAG_NAME,
|
|
167
|
+
}
|
|
168
|
+
by = by_map.get(selector_type)
|
|
169
|
+
if by is None:
|
|
170
|
+
raise BrowserError(f"Unknown selector type: {selector_type}")
|
|
171
|
+
try:
|
|
172
|
+
elements = driver.find_elements(by, selector)
|
|
173
|
+
if not elements:
|
|
174
|
+
return f"No elements found matching: {selector}"
|
|
175
|
+
results = []
|
|
176
|
+
for i, elem in enumerate(elements[:20], 1):
|
|
177
|
+
text = elem.text.strip()[:100] or elem.get_attribute("value") or ""
|
|
178
|
+
results.append(f"{i}. {text}")
|
|
179
|
+
return f"Found {len(elements)} elements:\n" + "\n".join(results)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
raise BrowserError(f"Could not find elements '{selector}': {exc}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def execute_script(script: str) -> str:
|
|
185
|
+
driver = _get_driver()
|
|
186
|
+
try:
|
|
187
|
+
result = driver.execute_script(script)
|
|
188
|
+
return f"Script executed. Result: {result}"
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
raise BrowserError(f"Script execution failed: {exc}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def close_browser() -> str:
|
|
194
|
+
global _driver, _browser_type
|
|
195
|
+
if _driver is not None:
|
|
196
|
+
try:
|
|
197
|
+
_driver.quit()
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
_driver = None
|
|
201
|
+
_browser_type = None
|
|
202
|
+
return "Browser closed"
|
|
203
|
+
return "No browser open"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_current_browser() -> str:
|
|
207
|
+
if _driver is None:
|
|
208
|
+
return "No browser open"
|
|
209
|
+
return _browser_type or "unknown"
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Browser skills: the complete AI-facing surface for web automation.
|
|
2
|
+
|
|
3
|
+
Each skill here takes a *goal* ("play Love Me Thoda Aur on YouTube") and hands
|
|
4
|
+
it to the deterministic :class:`~.browser_engine.BrowserEngine`, which performs
|
|
5
|
+
every step — navigation, popup dismissal, result selection, playback, and
|
|
6
|
+
verification — without returning to the AI in between. The AI issues exactly
|
|
7
|
+
one call per goal and reads back one verified outcome.
|
|
8
|
+
|
|
9
|
+
This is the boundary the refactor exists to enforce. There is deliberately no
|
|
10
|
+
skill that clicks "a thing on a web page": if a workflow is worth automating it
|
|
11
|
+
gets a named skill whose procedure lives here in code, where it is testable,
|
|
12
|
+
repeatable, and identical on every run. ``ui_*`` actions are refused outright
|
|
13
|
+
on browser windows (see :mod:`.dispatcher`) so the old click-by-click pattern
|
|
14
|
+
cannot come back.
|
|
15
|
+
|
|
16
|
+
Importing this module registers the skills; :mod:`.catalog` imports it.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ..tools.permissions import PermissionLevel
|
|
24
|
+
from .browser_engine import BrowserEngine, BrowserWorkflowError, WorkflowResult
|
|
25
|
+
from .skills import Outcome, SkillContext, SkillError, skill
|
|
26
|
+
|
|
27
|
+
# One engine per controller. Skills are dispatched one at a time against the
|
|
28
|
+
# session's single controller, so caching here keeps the DevTools connection
|
|
29
|
+
# and popup state alive across calls instead of re-attaching every skill.
|
|
30
|
+
_engines: "dict[int, BrowserEngine]" = {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _engine(ctx: SkillContext) -> BrowserEngine:
|
|
34
|
+
"""The BrowserEngine bound to this context's controller."""
|
|
35
|
+
key = id(ctx.controller)
|
|
36
|
+
engine = _engines.get(key)
|
|
37
|
+
if engine is None:
|
|
38
|
+
engine = BrowserEngine(controller=ctx.controller)
|
|
39
|
+
_engines[key] = engine
|
|
40
|
+
return engine
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def reset_engines() -> None:
|
|
44
|
+
"""Drop cached engines (session teardown and tests)."""
|
|
45
|
+
_engines.clear()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bind_engine(controller: Any, engine: BrowserEngine) -> None:
|
|
49
|
+
"""Pre-bind a BrowserEngine to a controller.
|
|
50
|
+
|
|
51
|
+
Lets a caller (notably the test suite) supply an engine with injected
|
|
52
|
+
drivers, so browser skills can be exercised without a live browser.
|
|
53
|
+
"""
|
|
54
|
+
_engines[id(controller)] = engine
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _run(ctx: SkillContext, action, description: str) -> Outcome:
|
|
58
|
+
"""Execute a workflow and translate it into a verifiable Outcome.
|
|
59
|
+
|
|
60
|
+
Every browser skill funnels through here so failure handling, state
|
|
61
|
+
recording, and the expectation contract are identical across the catalog.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
result: WorkflowResult = action()
|
|
65
|
+
except BrowserWorkflowError as exc:
|
|
66
|
+
# A workflow failure is already human-readable and actionable; surface
|
|
67
|
+
# it as a SkillError so the dispatcher can log and replan on it.
|
|
68
|
+
raise SkillError(str(exc))
|
|
69
|
+
ctx.state.record_action(description)
|
|
70
|
+
if result.url:
|
|
71
|
+
ctx.state.set_browser_url(result.url)
|
|
72
|
+
return Outcome(result.detail, result.expected, app_target="browser")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _query(params: dict[str, Any]) -> str:
|
|
76
|
+
"""The search phrase, accepting the names an AI naturally reaches for."""
|
|
77
|
+
for key in ("query", "q", "search", "text", "title", "song"):
|
|
78
|
+
value = str(params.get(key, "")).strip()
|
|
79
|
+
if value:
|
|
80
|
+
return value
|
|
81
|
+
raise SkillError("this skill requires a 'query' parameter")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- search ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@skill(
|
|
87
|
+
"youtube_search",
|
|
88
|
+
"Search YouTube for a query. Opens the results page and clears any consent "
|
|
89
|
+
"or sign-in popups automatically.",
|
|
90
|
+
PermissionLevel.DESKTOP,
|
|
91
|
+
{"query": "what to search for on YouTube"},
|
|
92
|
+
)
|
|
93
|
+
def youtube_search(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
94
|
+
query = _query(params)
|
|
95
|
+
return _run(
|
|
96
|
+
ctx, lambda: _engine(ctx).youtube_search(query), f"searched YouTube for {query}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@skill(
|
|
101
|
+
"youtube_play",
|
|
102
|
+
"Play a song or video on YouTube by name — the complete workflow. Finds the "
|
|
103
|
+
"best match, opens it, dismisses popups, and starts playback. Use this "
|
|
104
|
+
"instead of searching and then clicking a result.",
|
|
105
|
+
PermissionLevel.DESKTOP,
|
|
106
|
+
{"query": "the song, video, or channel to play"},
|
|
107
|
+
)
|
|
108
|
+
def youtube_play(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
109
|
+
query = _query(params)
|
|
110
|
+
return _run(ctx, lambda: _engine(ctx).youtube_play(query), f"played {query} on YouTube")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@skill(
|
|
114
|
+
"google_search",
|
|
115
|
+
"Search Google for a query in the default browser.",
|
|
116
|
+
PermissionLevel.DESKTOP,
|
|
117
|
+
{"query": "what to search for"},
|
|
118
|
+
)
|
|
119
|
+
def google_search(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
120
|
+
query = _query(params)
|
|
121
|
+
return _run(
|
|
122
|
+
ctx, lambda: _engine(ctx).google_search(query), f"searched Google for {query}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@skill(
|
|
127
|
+
"web_search",
|
|
128
|
+
"Search the web in the default browser (google, bing, duckduckgo, youtube).",
|
|
129
|
+
PermissionLevel.DESKTOP,
|
|
130
|
+
{"query": "search terms", "engine": "(optional) google|bing|duckduckgo|youtube"},
|
|
131
|
+
)
|
|
132
|
+
def web_search(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
133
|
+
query = _query(params)
|
|
134
|
+
engine_name = str(params.get("engine", "google")).strip().lower() or "google"
|
|
135
|
+
return _run(
|
|
136
|
+
ctx,
|
|
137
|
+
lambda: _engine(ctx).search(query, engine_name),
|
|
138
|
+
f"searched {engine_name} for {query}",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --- navigation --------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
@skill(
|
|
145
|
+
"open_url",
|
|
146
|
+
"Open a web address in the default browser and confirm the page loaded.",
|
|
147
|
+
PermissionLevel.DESKTOP,
|
|
148
|
+
{"url": "the address to open", "new_tab": "(optional) true to use a new tab"},
|
|
149
|
+
)
|
|
150
|
+
def open_url(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
151
|
+
url = str(params.get("url") or params.get("address") or "").strip()
|
|
152
|
+
if not url:
|
|
153
|
+
raise SkillError("open_url requires a 'url' parameter")
|
|
154
|
+
new_tab = _flag(params, "new_tab")
|
|
155
|
+
return _run(
|
|
156
|
+
ctx, lambda: _engine(ctx).open_url(url, new_tab=new_tab), f"opened {url}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@skill(
|
|
161
|
+
"new_tab",
|
|
162
|
+
"Open a new browser tab, optionally at a URL.",
|
|
163
|
+
PermissionLevel.DESKTOP,
|
|
164
|
+
{"url": "(optional) address to open in the new tab"},
|
|
165
|
+
)
|
|
166
|
+
def new_tab(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
167
|
+
url = str(params.get("url", "")).strip() or "about:blank"
|
|
168
|
+
return _run(ctx, lambda: _engine(ctx).new_tab(url), "opened a new browser tab")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@skill(
|
|
172
|
+
"close_tab",
|
|
173
|
+
"Close the active browser tab.",
|
|
174
|
+
PermissionLevel.DESKTOP,
|
|
175
|
+
)
|
|
176
|
+
def close_tab(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
177
|
+
return _run(ctx, lambda: _engine(ctx).close_tab(), "closed a browser tab")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@skill(
|
|
181
|
+
"switch_tab",
|
|
182
|
+
"Switch to another browser tab, by part of its title or URL (or the next "
|
|
183
|
+
"tab when no target is given).",
|
|
184
|
+
PermissionLevel.DESKTOP,
|
|
185
|
+
{"target": "(optional) part of the tab's title or address"},
|
|
186
|
+
)
|
|
187
|
+
def switch_tab(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
188
|
+
target = str(params.get("target") or params.get("title") or params.get("url") or "").strip()
|
|
189
|
+
return _run(
|
|
190
|
+
ctx,
|
|
191
|
+
lambda: _engine(ctx).switch_tab(target),
|
|
192
|
+
f"switched to tab {target}" if target else "switched browser tab",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@skill("back", "Go back one page in the browser's history.", PermissionLevel.DESKTOP)
|
|
197
|
+
def back(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
198
|
+
return _run(ctx, lambda: _engine(ctx).back(), "went back in the browser")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@skill("forward", "Go forward one page in the browser's history.", PermissionLevel.DESKTOP)
|
|
202
|
+
def forward(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
203
|
+
return _run(ctx, lambda: _engine(ctx).forward(), "went forward in the browser")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@skill("refresh", "Reload the current browser page.", PermissionLevel.DESKTOP)
|
|
207
|
+
def refresh(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
208
|
+
return _run(ctx, lambda: _engine(ctx).refresh(), "refreshed the browser page")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# --- information -------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
@skill(
|
|
214
|
+
"browser_page",
|
|
215
|
+
"Report the title and address of the page currently open in the browser.",
|
|
216
|
+
PermissionLevel.DESKTOP,
|
|
217
|
+
)
|
|
218
|
+
def browser_page(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
219
|
+
return _run(ctx, lambda: _engine(ctx).page_info(), "read the current browser page")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@skill(
|
|
223
|
+
"which_browser",
|
|
224
|
+
"Report which browser is the system default.",
|
|
225
|
+
PermissionLevel.DESKTOP,
|
|
226
|
+
)
|
|
227
|
+
def which_browser(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
228
|
+
return Outcome(f"default browser: {_engine(ctx).which_browser()}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@skill(
|
|
232
|
+
"launch_browser",
|
|
233
|
+
"Open the user's default browser, optionally at a URL.",
|
|
234
|
+
PermissionLevel.DESKTOP,
|
|
235
|
+
{"url": "(optional) URL to open"},
|
|
236
|
+
)
|
|
237
|
+
def launch_browser(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
238
|
+
url = str(params.get("url", "")).strip()
|
|
239
|
+
if not url or url == "about:blank":
|
|
240
|
+
return _run(ctx, lambda: _engine(ctx).new_tab("about:blank"), "opened the browser")
|
|
241
|
+
return _run(ctx, lambda: _engine(ctx).open_url(url), f"opened browser at {url}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _flag(params: dict[str, Any], key: str) -> bool:
|
|
245
|
+
return str(params.get(key, "")).strip().lower() in ("true", "1", "yes", "on")
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""The built-in skill catalog.
|
|
2
|
+
|
|
3
|
+
Concrete, deterministic skills registered into ``skills.REGISTRY``. Each is a
|
|
4
|
+
real procedure over the :class:`ComputerController` primitives — launching apps,
|
|
5
|
+
creating projects, editor/clipboard/git actions — with a declared permission
|
|
6
|
+
level and an ``expected`` outcome for the verifier. No placeholders, no AI
|
|
7
|
+
calls.
|
|
8
|
+
|
|
9
|
+
Browser work lives in its own module (:mod:`.browser_skills`) because it is a
|
|
10
|
+
whole workflow engine rather than a handful of primitives; importing this
|
|
11
|
+
module pulls those skills in too.
|
|
12
|
+
|
|
13
|
+
Importing this module has the side effect of populating the registry; the
|
|
14
|
+
Computer Engine imports it once at startup.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ..tools.permissions import PermissionLevel, CATEGORY_SHELL
|
|
24
|
+
from .skills import Outcome, SkillContext, SkillError, skill
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _req(params: dict[str, Any], key: str) -> str:
|
|
28
|
+
val = str(params.get(key, "")).strip()
|
|
29
|
+
if not val:
|
|
30
|
+
raise SkillError(f"skill requires a '{key}' parameter")
|
|
31
|
+
return val
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --- application launching --------------------------------------------------
|
|
35
|
+
|
|
36
|
+
@skill("launch_app", "Open or focus a desktop application by name or path.",
|
|
37
|
+
PermissionLevel.DESKTOP, {"target": "app name, e.g. 'notepad' or a path"})
|
|
38
|
+
def launch_app(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
39
|
+
target = _req(params, "target")
|
|
40
|
+
# Already open? Focus it instead of spawning a duplicate.
|
|
41
|
+
for w in ctx.controller.list_windows():
|
|
42
|
+
if target.lower() in w.title.lower():
|
|
43
|
+
ctx.controller.focus_window(w.title)
|
|
44
|
+
ctx.state.note_focus(w.title)
|
|
45
|
+
return Outcome(f"focused existing {w.title}", {"window": target},
|
|
46
|
+
window_title=w.title, app_target=target)
|
|
47
|
+
ctx.controller.open_app(target)
|
|
48
|
+
ctx.state.record_action(f"launched {target}")
|
|
49
|
+
ctx.state.note_focus(target)
|
|
50
|
+
return Outcome(f"launched {target}", {"window": target},
|
|
51
|
+
window_title=target, app_target=target)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@skill("focus_app", "Bring an already-open application to the foreground.",
|
|
55
|
+
PermissionLevel.DESKTOP, {"target": "part of the window title"})
|
|
56
|
+
def focus_app(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
57
|
+
target = _req(params, "target")
|
|
58
|
+
win = ctx.controller.focus_window(target)
|
|
59
|
+
title = getattr(win, "title", target)
|
|
60
|
+
ctx.state.note_focus(title)
|
|
61
|
+
return Outcome(f"focused {title}", {"window": target}, window_title=title)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@skill("close_app", "Close an application window.",
|
|
65
|
+
PermissionLevel.DESKTOP, {"target": "part of the window title"})
|
|
66
|
+
def close_app(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
67
|
+
target = _req(params, "target")
|
|
68
|
+
ctx.controller.close_app(target)
|
|
69
|
+
ctx.state.record_action(f"closed {target}")
|
|
70
|
+
return Outcome(f"closed {target}", {"window_gone": target})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@skill("launch_vscode", "Open Visual Studio Code, optionally on a folder.",
|
|
74
|
+
PermissionLevel.DESKTOP, {"path": "(optional) folder to open"})
|
|
75
|
+
def launch_vscode(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
76
|
+
path = str(params.get("path", "")).strip()
|
|
77
|
+
target = f'code "{path}"' if path else "code"
|
|
78
|
+
ctx.controller.open_app(target)
|
|
79
|
+
ctx.state.note_focus("Visual Studio Code")
|
|
80
|
+
return Outcome(f"opened VS Code{f' on {path}' if path else ''}",
|
|
81
|
+
{"window": "Visual Studio Code"},
|
|
82
|
+
window_title="Visual Studio Code", app_target="code")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@skill("launch_terminal", "Open a terminal window.", PermissionLevel.DESKTOP)
|
|
86
|
+
def launch_terminal(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
87
|
+
# Windows Terminal if present, else the classic console.
|
|
88
|
+
target = "wt" if os.name == "nt" else "x-terminal-emulator"
|
|
89
|
+
try:
|
|
90
|
+
ctx.controller.open_app(target)
|
|
91
|
+
except Exception:
|
|
92
|
+
ctx.controller.open_app("cmd" if os.name == "nt" else "xterm")
|
|
93
|
+
ctx.state.note_focus("Terminal")
|
|
94
|
+
return Outcome("opened a terminal", {"window": "Terminal"}, app_target=target)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --- browser ----------------------------------------------------------------
|
|
98
|
+
# Every web workflow lives in :mod:`.browser_skills`, backed by the
|
|
99
|
+
# deterministic BrowserEngine: importing it registers youtube_play,
|
|
100
|
+
# youtube_search, google_search, open_url, new_tab/close_tab/switch_tab, and
|
|
101
|
+
# back/forward/refresh. They are whole procedures — popup handling, result
|
|
102
|
+
# selection, and verification included — so the AI issues one goal per task
|
|
103
|
+
# and never assembles a browser workflow out of clicks.
|
|
104
|
+
from . import browser_skills as _browser_skills # noqa: E402,F401
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- operator skills (Phase 4+) ---------------------------------------------
|
|
108
|
+
# Screen intelligence queries, semantic element-id actions, the application
|
|
109
|
+
# controller (open/find/close/install), DOM web extraction, and local
|
|
110
|
+
# memory. Same registry, same permission gates, same verification path.
|
|
111
|
+
from . import operator_skills as _operator_skills # noqa: E402,F401
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- editor / clipboard -----------------------------------------------------
|
|
115
|
+
|
|
116
|
+
@skill("save_current_file", "Save the file in the focused editor (Ctrl+S).",
|
|
117
|
+
PermissionLevel.DESKTOP)
|
|
118
|
+
def save_current_file(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
119
|
+
ctx.controller.hotkey(["ctrl", "s"])
|
|
120
|
+
ctx.state.record_action("saved current file")
|
|
121
|
+
return Outcome("sent save (Ctrl+S) to the focused window")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@skill("copy_selection", "Copy the current selection to the clipboard.",
|
|
125
|
+
PermissionLevel.DESKTOP)
|
|
126
|
+
def copy_selection(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
127
|
+
ctx.controller.hotkey(["ctrl", "c"])
|
|
128
|
+
ctx.state.record_action("copied selection")
|
|
129
|
+
return Outcome("copied selection to clipboard")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@skill("paste_clipboard", "Paste the clipboard into the focused window.",
|
|
133
|
+
PermissionLevel.DESKTOP)
|
|
134
|
+
def paste_clipboard(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
135
|
+
ctx.controller.hotkey(["ctrl", "v"])
|
|
136
|
+
ctx.state.record_action("pasted clipboard")
|
|
137
|
+
return Outcome("pasted clipboard into the focused window")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- filesystem / projects (workspace level) --------------------------------
|
|
141
|
+
|
|
142
|
+
@skill("create_python_project", "Scaffold a Python project folder with main.py and a venv-ready layout.",
|
|
143
|
+
PermissionLevel.WORKSPACE, {"name": "project folder name", "path": "(optional) parent dir"})
|
|
144
|
+
def create_python_project(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
145
|
+
name = _req(params, "name")
|
|
146
|
+
parent = ctx.permissions.resolve(str(params.get("path", ".")))
|
|
147
|
+
root = parent / name
|
|
148
|
+
ctx.permissions.check_write(root)
|
|
149
|
+
(root / "src").mkdir(parents=True, exist_ok=True)
|
|
150
|
+
(root / "src" / "main.py").write_text(
|
|
151
|
+
'def main() -> None:\n print("Hello from ' + name + '")\n\n\n'
|
|
152
|
+
'if __name__ == "__main__":\n main()\n',
|
|
153
|
+
encoding="utf-8",
|
|
154
|
+
)
|
|
155
|
+
(root / "README.md").write_text(f"# {name}\n", encoding="utf-8")
|
|
156
|
+
(root / "requirements.txt").write_text("", encoding="utf-8")
|
|
157
|
+
ctx.state.record_action(f"created python project {name}")
|
|
158
|
+
return Outcome(f"created Python project at {root}", {"file_exists": str(root / "src" / "main.py")})
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- terminal / commands (require confirmation) -----------------------------
|
|
162
|
+
|
|
163
|
+
@skill("run_python", "Run a Python script with the system interpreter.",
|
|
164
|
+
PermissionLevel.WORKSPACE, {"script": "path to the .py file"}, sensitive=True)
|
|
165
|
+
def run_python(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
166
|
+
script = _req(params, "script")
|
|
167
|
+
path = ctx.permissions.resolve(script)
|
|
168
|
+
ctx.permissions.check_execute(f"python {path}")
|
|
169
|
+
ctx.permissions.confirm_action(CATEGORY_SHELL, f"python {path}")
|
|
170
|
+
out = _run_command(["python", str(path)], cwd=ctx.permissions.workspace, ctx=ctx)
|
|
171
|
+
return Outcome(f"ran {path}\n{out}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@skill("run_node", "Run a JavaScript file with Node.js.",
|
|
175
|
+
PermissionLevel.WORKSPACE, {"script": "path to the .js file"}, sensitive=True)
|
|
176
|
+
def run_node(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
177
|
+
script = _req(params, "script")
|
|
178
|
+
path = ctx.permissions.resolve(script)
|
|
179
|
+
ctx.permissions.check_execute(f"node {path}")
|
|
180
|
+
ctx.permissions.confirm_action(CATEGORY_SHELL, f"node {path}")
|
|
181
|
+
out = _run_command(["node", str(path)], cwd=ctx.permissions.workspace, ctx=ctx)
|
|
182
|
+
return Outcome(f"ran {path}\n{out}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _run_command(argv: list[str], *, cwd: Path, ctx: SkillContext) -> str:
|
|
186
|
+
import subprocess
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
proc = subprocess.run(
|
|
190
|
+
argv, cwd=str(cwd), capture_output=True, text=True, timeout=120
|
|
191
|
+
)
|
|
192
|
+
except FileNotFoundError:
|
|
193
|
+
raise SkillError(f"'{argv[0]}' is not installed or not on PATH")
|
|
194
|
+
except subprocess.TimeoutExpired:
|
|
195
|
+
raise SkillError(f"'{' '.join(argv)}' timed out after 120s")
|
|
196
|
+
output = (proc.stdout or "") + (proc.stderr or "")
|
|
197
|
+
if ctx.permissions.on_output and output:
|
|
198
|
+
for line in output.splitlines():
|
|
199
|
+
ctx.permissions.on_output(line)
|
|
200
|
+
return output.strip()[:4000] or f"(exit {proc.returncode}, no output)"
|