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.
Files changed (114) hide show
  1. seedcode/__init__.py +14 -0
  2. seedcode/__main__.py +12 -0
  3. seedcode/app.py +508 -0
  4. seedcode/apps/__init__.py +32 -0
  5. seedcode/apps/discovery.py +241 -0
  6. seedcode/apps/installer.py +164 -0
  7. seedcode/apps/launcher.py +156 -0
  8. seedcode/apps/verifier.py +119 -0
  9. seedcode/assets/logo.txt +15 -0
  10. seedcode/cli.py +95 -0
  11. seedcode/commands/__init__.py +81 -0
  12. seedcode/commands/about.py +34 -0
  13. seedcode/commands/agent.py +94 -0
  14. seedcode/commands/assist.py +201 -0
  15. seedcode/commands/clear.py +20 -0
  16. seedcode/commands/desktop.py +104 -0
  17. seedcode/commands/doctor.py +152 -0
  18. seedcode/commands/help.py +61 -0
  19. seedcode/commands/history.py +365 -0
  20. seedcode/commands/palette.py +100 -0
  21. seedcode/commands/provider.py +451 -0
  22. seedcode/commands/theme.py +76 -0
  23. seedcode/computer/__init__.py +98 -0
  24. seedcode/computer/browser.py +276 -0
  25. seedcode/computer/browser_cdp.py +567 -0
  26. seedcode/computer/browser_engine.py +546 -0
  27. seedcode/computer/browser_extract.py +301 -0
  28. seedcode/computer/browser_popups.py +329 -0
  29. seedcode/computer/browser_selenium.py +209 -0
  30. seedcode/computer/browser_skills.py +245 -0
  31. seedcode/computer/catalog.py +200 -0
  32. seedcode/computer/controller.py +324 -0
  33. seedcode/computer/dispatcher.py +272 -0
  34. seedcode/computer/dpi.py +185 -0
  35. seedcode/computer/engine.py +105 -0
  36. seedcode/computer/keyboard.py +101 -0
  37. seedcode/computer/logbook.py +104 -0
  38. seedcode/computer/mouse.py +48 -0
  39. seedcode/computer/ocr.py +213 -0
  40. seedcode/computer/operator_skills.py +577 -0
  41. seedcode/computer/permissions.py +203 -0
  42. seedcode/computer/recovery.py +115 -0
  43. seedcode/computer/registry.py +107 -0
  44. seedcode/computer/resolver.py +434 -0
  45. seedcode/computer/screen.py +130 -0
  46. seedcode/computer/screen_state.py +412 -0
  47. seedcode/computer/selfguard.py +197 -0
  48. seedcode/computer/semantic.py +100 -0
  49. seedcode/computer/skills.py +139 -0
  50. seedcode/computer/state.py +199 -0
  51. seedcode/computer/verifier.py +177 -0
  52. seedcode/computer/vision.py +327 -0
  53. seedcode/computer/windows.py +217 -0
  54. seedcode/config/__init__.py +8 -0
  55. seedcode/config/defaults.py +22 -0
  56. seedcode/config/manager.py +62 -0
  57. seedcode/core/__init__.py +31 -0
  58. seedcode/core/agent.py +534 -0
  59. seedcode/core/chat.py +128 -0
  60. seedcode/core/client.py +9 -0
  61. seedcode/core/errors.py +199 -0
  62. seedcode/core/identity.py +66 -0
  63. seedcode/core/identity_store.py +119 -0
  64. seedcode/core/lifecycle.py +240 -0
  65. seedcode/core/limits.py +35 -0
  66. seedcode/core/models.py +347 -0
  67. seedcode/core/project.py +96 -0
  68. seedcode/core/providers/__init__.py +58 -0
  69. seedcode/core/providers/aerolink.py +324 -0
  70. seedcode/core/providers/base.py +230 -0
  71. seedcode/core/providers/freemodel.py +931 -0
  72. seedcode/core/providers/ollama.py +262 -0
  73. seedcode/core/providers/openrouter.py +393 -0
  74. seedcode/core/streaming.py +21 -0
  75. seedcode/memory/__init__.py +8 -0
  76. seedcode/memory/manager.py +47 -0
  77. seedcode/memory/storage.py +38 -0
  78. seedcode/memory/store.py +257 -0
  79. seedcode/tools/__init__.py +35 -0
  80. seedcode/tools/base.py +179 -0
  81. seedcode/tools/desktop.py +371 -0
  82. seedcode/tools/filesystem.py +309 -0
  83. seedcode/tools/git.py +72 -0
  84. seedcode/tools/patch.py +170 -0
  85. seedcode/tools/permissions.py +288 -0
  86. seedcode/tools/search.py +137 -0
  87. seedcode/tools/terminal.py +200 -0
  88. seedcode/tools/textio.py +59 -0
  89. seedcode/ui/__init__.py +164 -0
  90. seedcode/ui/badges.py +64 -0
  91. seedcode/ui/banner.py +78 -0
  92. seedcode/ui/dashboard.py +197 -0
  93. seedcode/ui/dialog.py +62 -0
  94. seedcode/ui/fuzzy.py +128 -0
  95. seedcode/ui/layout.py +54 -0
  96. seedcode/ui/menu.py +61 -0
  97. seedcode/ui/palette.py +40 -0
  98. seedcode/ui/progress.py +41 -0
  99. seedcode/ui/prompts.py +16 -0
  100. seedcode/ui/renderer.py +36 -0
  101. seedcode/ui/searchbox.py +70 -0
  102. seedcode/ui/selector.py +514 -0
  103. seedcode/ui/statusbar.py +38 -0
  104. seedcode/ui/textbox.py +61 -0
  105. seedcode/ui/theme.py +204 -0
  106. seedcode/ui/tree.py +91 -0
  107. seedcode/utils/__init__.py +22 -0
  108. seedcode/utils/helpers.py +97 -0
  109. seedcode/utils/logger.py +65 -0
  110. seedcode_cli-6.1.5.dist-info/METADATA +368 -0
  111. seedcode_cli-6.1.5.dist-info/RECORD +114 -0
  112. seedcode_cli-6.1.5.dist-info/WHEEL +4 -0
  113. seedcode_cli-6.1.5.dist-info/entry_points.txt +2 -0
  114. 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)"