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,577 @@
|
|
|
1
|
+
"""Operator skills: screen intelligence, semantic UI, apps, web, memory.
|
|
2
|
+
|
|
3
|
+
The Phase-4+ operator layers exposed to the AI as permissioned skills, in
|
|
4
|
+
the same registry as the built-in catalog (:mod:`.catalog`). Each skill is
|
|
5
|
+
a thin, deterministic adapter:
|
|
6
|
+
|
|
7
|
+
* queries return **compact structured data** (ids, names, counts) — never
|
|
8
|
+
raw trees or screenshots;
|
|
9
|
+
* actions are **id/description-addressed** — the AI never supplies
|
|
10
|
+
coordinates, keystrokes, or selectors;
|
|
11
|
+
* failures surface as model-readable text (the structured error taxonomy's
|
|
12
|
+
messages), so the planner can re-plan; nothing here can terminate the
|
|
13
|
+
process (lifecycle) or touch SeedCode's own console (selfguard — the
|
|
14
|
+
underlying windows driver already refuses).
|
|
15
|
+
|
|
16
|
+
Importing this module registers the skills; :mod:`.catalog` imports it.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from ..core.errors import ApplicationNotFoundError, OperatorError
|
|
25
|
+
from ..tools.permissions import PermissionLevel
|
|
26
|
+
from .permissions import CATEGORY_INSTALL
|
|
27
|
+
from .skills import Outcome, SkillContext, SkillError, skill
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _compact(value: Any, limit: int = 3500) -> str:
|
|
31
|
+
"""Compact JSON rendering with a size bound for model-facing output."""
|
|
32
|
+
try:
|
|
33
|
+
text = json.dumps(value, ensure_ascii=False, default=str)
|
|
34
|
+
except (TypeError, ValueError):
|
|
35
|
+
text = str(value)
|
|
36
|
+
return text if len(text) <= limit else text[:limit] + " …[truncated]"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- screen intelligence queries ------------------------------------------------
|
|
40
|
+
|
|
41
|
+
@skill(
|
|
42
|
+
"screen_state",
|
|
43
|
+
"Structured screen state: active window, windows, monitors, and UI "
|
|
44
|
+
"elements with semantic element ids. Prefer over screenshots.",
|
|
45
|
+
PermissionLevel.DESKTOP,
|
|
46
|
+
{"include_elements": "(optional) false to omit the element list"},
|
|
47
|
+
)
|
|
48
|
+
def screen_state(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
49
|
+
from .screen_state import get_screen_engine
|
|
50
|
+
|
|
51
|
+
include_elements = str(params.get("include_elements", "true")).lower() not in (
|
|
52
|
+
"false", "0", "no"
|
|
53
|
+
)
|
|
54
|
+
state = get_screen_engine().state_json(include_elements=include_elements)
|
|
55
|
+
return Outcome(_compact(state))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@skill(
|
|
59
|
+
"get_active_window",
|
|
60
|
+
"The currently focused window with app name, title, pid, and bounds.",
|
|
61
|
+
PermissionLevel.DESKTOP,
|
|
62
|
+
)
|
|
63
|
+
def get_active_window(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
64
|
+
from .screen_state import get_screen_engine
|
|
65
|
+
|
|
66
|
+
win = get_screen_engine().active_window()
|
|
67
|
+
if win is None:
|
|
68
|
+
return Outcome("no focused window detected")
|
|
69
|
+
return Outcome(_compact({
|
|
70
|
+
"id": win.id, "app": _app_of(win.title), "title": win.title,
|
|
71
|
+
"pid": win.pid, "bounds": win.bounds,
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@skill(
|
|
76
|
+
"list_windows",
|
|
77
|
+
"All visible top-level windows with app, title, pid, and focus state.",
|
|
78
|
+
PermissionLevel.DESKTOP,
|
|
79
|
+
)
|
|
80
|
+
def list_windows_query(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
81
|
+
from .screen_state import get_screen_engine
|
|
82
|
+
|
|
83
|
+
wins = get_screen_engine().list_windows()
|
|
84
|
+
return Outcome(_compact([
|
|
85
|
+
{"id": w.id, "app": _app_of(w.title), "title": w.title, "pid": w.pid,
|
|
86
|
+
"focused": w.focused, "minimized": w.minimized}
|
|
87
|
+
for w in wins
|
|
88
|
+
]))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@skill(
|
|
92
|
+
"find_ui_element",
|
|
93
|
+
"Find a UI element by name/role (e.g. \"Play button\") and return its "
|
|
94
|
+
"semantic element id. The id is what click/type actions take.",
|
|
95
|
+
PermissionLevel.DESKTOP,
|
|
96
|
+
{"query": "name or description, e.g. 'Play' or 'search box'",
|
|
97
|
+
"window": "(optional) window title fragment to scan"},
|
|
98
|
+
)
|
|
99
|
+
def find_ui_element(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
100
|
+
from .screen_state import get_screen_engine
|
|
101
|
+
|
|
102
|
+
query = str(params.get("query", "")).strip()
|
|
103
|
+
if not query:
|
|
104
|
+
raise SkillError("find_ui_element requires a 'query' parameter")
|
|
105
|
+
window = str(params.get("window", "")).strip() or None
|
|
106
|
+
el = get_screen_engine().find_element(query, window=window, fresh=True)
|
|
107
|
+
return Outcome(_compact({
|
|
108
|
+
"id": el.id, "role": el.role, "name": el.name, "enabled": el.enabled,
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@skill(
|
|
113
|
+
"get_focused_element",
|
|
114
|
+
"The element in the active window that currently has keyboard focus.",
|
|
115
|
+
PermissionLevel.DESKTOP,
|
|
116
|
+
)
|
|
117
|
+
def get_focused_element(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
118
|
+
from .screen_state import get_screen_engine
|
|
119
|
+
|
|
120
|
+
engine = get_screen_engine()
|
|
121
|
+
# The UIA tree does not expose focus state per element in the light
|
|
122
|
+
# walker; report the focused window plus its first focusable candidate.
|
|
123
|
+
win = engine.active_window()
|
|
124
|
+
if win is None:
|
|
125
|
+
return Outcome("no focused window")
|
|
126
|
+
state = engine.state_json(include_elements=True)
|
|
127
|
+
elements = state.get("elements", [])
|
|
128
|
+
return Outcome(_compact({
|
|
129
|
+
"window": {"id": win.id, "title": win.title},
|
|
130
|
+
"elements": elements[:30],
|
|
131
|
+
}))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _app_of(title: str) -> str:
|
|
135
|
+
for sep in (" — ", " - ", " – "):
|
|
136
|
+
if sep in title:
|
|
137
|
+
return title.rsplit(sep, 1)[-1].strip() or title.strip()
|
|
138
|
+
return title.strip()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# --- semantic element actions ---------------------------------------------------
|
|
142
|
+
|
|
143
|
+
@skill(
|
|
144
|
+
"click_element",
|
|
145
|
+
"Click a UI element by its semantic element id (from find_ui_element).",
|
|
146
|
+
PermissionLevel.DESKTOP,
|
|
147
|
+
{"element_id": "element id from a screen query", "double": "(optional) true to double-click"},
|
|
148
|
+
)
|
|
149
|
+
def click_element(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
150
|
+
from .semantic import SemanticActions
|
|
151
|
+
|
|
152
|
+
element_id = _require_id(params)
|
|
153
|
+
double = str(params.get("double", "")).lower() in ("true", "1", "yes")
|
|
154
|
+
actions = SemanticActions()
|
|
155
|
+
detail = actions.click(element_id, double=double)
|
|
156
|
+
ctx.state.record_action(detail)
|
|
157
|
+
return Outcome(detail, {"element": element_id})
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@skill(
|
|
161
|
+
"type_into_element",
|
|
162
|
+
"Type text into a UI element by element id (focuses it first).",
|
|
163
|
+
PermissionLevel.DESKTOP,
|
|
164
|
+
{"element_id": "element id from a screen query", "text": "text to type",
|
|
165
|
+
"secret": "(optional) true for passwords"},
|
|
166
|
+
)
|
|
167
|
+
def type_into_element(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
168
|
+
from .semantic import SemanticActions
|
|
169
|
+
|
|
170
|
+
element_id = _require_id(params)
|
|
171
|
+
text = str(params.get("text", ""))
|
|
172
|
+
if not text:
|
|
173
|
+
raise SkillError("type_into_element requires 'text'")
|
|
174
|
+
secret = str(params.get("secret", "")).lower() in ("true", "1", "yes")
|
|
175
|
+
actions = SemanticActions()
|
|
176
|
+
detail = actions.type_into(element_id, text, secret=secret)
|
|
177
|
+
ctx.state.record_action(detail)
|
|
178
|
+
return Outcome(detail, {"element": element_id})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@skill(
|
|
182
|
+
"focus_element",
|
|
183
|
+
"Move keyboard focus to a UI element by element id.",
|
|
184
|
+
PermissionLevel.DESKTOP,
|
|
185
|
+
{"element_id": "element id from a screen query"},
|
|
186
|
+
)
|
|
187
|
+
def focus_element(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
188
|
+
from .semantic import SemanticActions
|
|
189
|
+
|
|
190
|
+
detail = SemanticActions().focus(_require_id(params))
|
|
191
|
+
ctx.state.record_action(detail)
|
|
192
|
+
return Outcome(detail)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@skill(
|
|
196
|
+
"press_element",
|
|
197
|
+
"Focus an element by id, then send a keyboard shortcut to it.",
|
|
198
|
+
PermissionLevel.DESKTOP,
|
|
199
|
+
{"element_id": "element id from a screen query",
|
|
200
|
+
"keys": "e.g. \"enter\" or \"ctrl+s\""},
|
|
201
|
+
)
|
|
202
|
+
def press_element(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
203
|
+
from .semantic import SemanticActions
|
|
204
|
+
|
|
205
|
+
element_id = _require_id(params)
|
|
206
|
+
keys = [k.strip() for k in str(params.get("keys", "")).replace("+", " ").split() if k.strip()]
|
|
207
|
+
if not keys:
|
|
208
|
+
raise SkillError("press_element requires 'keys'")
|
|
209
|
+
detail = SemanticActions().press(element_id, keys)
|
|
210
|
+
ctx.state.record_action(detail)
|
|
211
|
+
return Outcome(detail)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _require_id(params: dict[str, Any]) -> str:
|
|
215
|
+
element_id = str(params.get("element_id", "")).strip()
|
|
216
|
+
if not element_id:
|
|
217
|
+
raise SkillError("this action requires an 'element_id' from a screen query")
|
|
218
|
+
return element_id
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# --- application controller -------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
@skill(
|
|
224
|
+
"app_open",
|
|
225
|
+
"Open an application by name: reuses/focuses a running instance, "
|
|
226
|
+
"launches via Start Menu shortcut or registered executable, then "
|
|
227
|
+
"verifies a window appeared. Errors list trusted install options.",
|
|
228
|
+
PermissionLevel.DESKTOP,
|
|
229
|
+
{"target": "application name, e.g. 'Spotify' or 'Calculator'"},
|
|
230
|
+
)
|
|
231
|
+
def app_open(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
232
|
+
from ..apps.launcher import launch_app
|
|
233
|
+
|
|
234
|
+
target = str(params.get("target", "")).strip()
|
|
235
|
+
if not target:
|
|
236
|
+
raise SkillError("app_open requires a 'target' parameter")
|
|
237
|
+
try:
|
|
238
|
+
result = launch_app(target)
|
|
239
|
+
except ApplicationNotFoundError as exc:
|
|
240
|
+
# Missing app: surface the install option; installation is a
|
|
241
|
+
# separate, separately-confirmed skill (install_app).
|
|
242
|
+
raise SkillError(
|
|
243
|
+
f"{exc} Ask the user whether to install it (install_app)."
|
|
244
|
+
)
|
|
245
|
+
ctx.state.record_action(result.describe())
|
|
246
|
+
return Outcome(result.describe())
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@skill(
|
|
250
|
+
"app_find",
|
|
251
|
+
"Check whether an application is installed and how it would be launched.",
|
|
252
|
+
PermissionLevel.DESKTOP,
|
|
253
|
+
{"target": "application name"},
|
|
254
|
+
)
|
|
255
|
+
def app_find(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
256
|
+
from ..apps.discovery import find_app
|
|
257
|
+
|
|
258
|
+
target = str(params.get("target", "")).strip()
|
|
259
|
+
if not target:
|
|
260
|
+
raise SkillError("app_find requires a 'target' parameter")
|
|
261
|
+
try:
|
|
262
|
+
app = find_app(target)
|
|
263
|
+
except ApplicationNotFoundError as exc:
|
|
264
|
+
raise SkillError(str(exc))
|
|
265
|
+
return Outcome(_compact({
|
|
266
|
+
"name": app.name, "source": app.source, "target": app.target,
|
|
267
|
+
"exe": app.exe, "install_location": app.install_location,
|
|
268
|
+
}))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@skill(
|
|
272
|
+
"app_close",
|
|
273
|
+
"Close an application window by name. Refuses SeedCode's own terminal.",
|
|
274
|
+
PermissionLevel.DESKTOP,
|
|
275
|
+
{"target": "part of the window title"},
|
|
276
|
+
)
|
|
277
|
+
def app_close(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
278
|
+
target = _req(params, "target")
|
|
279
|
+
ctx.controller.close_app(target)
|
|
280
|
+
ctx.state.record_action(f"closed {target}")
|
|
281
|
+
return Outcome(f"closed {target}", {"window_gone": target})
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@skill(
|
|
285
|
+
"install_app",
|
|
286
|
+
"Install a missing application via a trusted source (winget). Always "
|
|
287
|
+
"asks the user first with the exact package and source shown.",
|
|
288
|
+
PermissionLevel.DESKTOP,
|
|
289
|
+
{"target": "application name", "confirm": "(optional) skip asking (ignored)"},
|
|
290
|
+
)
|
|
291
|
+
def install_app_skill(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
292
|
+
from ..apps.installer import install_app
|
|
293
|
+
|
|
294
|
+
target = str(params.get("target", "")).strip()
|
|
295
|
+
if not target:
|
|
296
|
+
raise SkillError("install_app requires a 'target' parameter")
|
|
297
|
+
|
|
298
|
+
# The INSTALL category is sensitive: the gate re-asks every time and the
|
|
299
|
+
# model cannot mark anything trusted (installer.py refuses unknown
|
|
300
|
+
# sources). The confirm callback is the session's desktop gate.
|
|
301
|
+
def _confirm(plan_text: str) -> bool:
|
|
302
|
+
try:
|
|
303
|
+
ctx.permissions.desktop.check(CATEGORY_INSTALL, plan_text) # type: ignore[union-attr]
|
|
304
|
+
return True
|
|
305
|
+
except Exception:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
result = install_app(target, confirm=_confirm)
|
|
309
|
+
ctx.state.record_action(result.detail)
|
|
310
|
+
if not result.success:
|
|
311
|
+
raise SkillError(result.detail)
|
|
312
|
+
return Outcome(result.detail)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _req(params: dict[str, Any], key: str) -> str:
|
|
316
|
+
val = str(params.get(key, "")).strip()
|
|
317
|
+
if not val:
|
|
318
|
+
raise SkillError(f"skill requires a '{key}' parameter")
|
|
319
|
+
return val
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# --- web intelligence -----------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
@skill(
|
|
325
|
+
"web_page_info",
|
|
326
|
+
"Current page url and title from the browser's DOM.",
|
|
327
|
+
PermissionLevel.DESKTOP,
|
|
328
|
+
)
|
|
329
|
+
def web_page_info(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
330
|
+
from .browser_extract import get_web_extractor
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
info = get_web_extractor().page_info()
|
|
334
|
+
except OperatorError as exc:
|
|
335
|
+
raise SkillError(str(exc))
|
|
336
|
+
return Outcome(_compact(info))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@skill(
|
|
340
|
+
"web_extract_text",
|
|
341
|
+
"Extract the page's readable text (DOM, no screenshots).",
|
|
342
|
+
PermissionLevel.DESKTOP,
|
|
343
|
+
)
|
|
344
|
+
def web_extract_text(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
345
|
+
from .browser_extract import get_web_extractor
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
extraction = get_web_extractor().page_text()
|
|
349
|
+
except OperatorError as exc:
|
|
350
|
+
raise SkillError(str(exc))
|
|
351
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@skill(
|
|
355
|
+
"web_extract_headings",
|
|
356
|
+
"Extract the page's headings (h1-h6) with their levels.",
|
|
357
|
+
PermissionLevel.DESKTOP,
|
|
358
|
+
)
|
|
359
|
+
def web_extract_headings(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
360
|
+
from .browser_extract import get_web_extractor
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
extraction = get_web_extractor().headings()
|
|
364
|
+
except OperatorError as exc:
|
|
365
|
+
raise SkillError(str(exc))
|
|
366
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@skill(
|
|
370
|
+
"web_extract_links",
|
|
371
|
+
"Extract visible links; optionally filter by a text fragment.",
|
|
372
|
+
PermissionLevel.DESKTOP,
|
|
373
|
+
{"filter": "(optional) keep links whose text/href contains this"},
|
|
374
|
+
)
|
|
375
|
+
def web_extract_links(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
376
|
+
from .browser_extract import get_web_extractor
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
extraction = get_web_extractor().links(
|
|
380
|
+
filter_text=str(params.get("filter", "")).strip()
|
|
381
|
+
)
|
|
382
|
+
except OperatorError as exc:
|
|
383
|
+
raise SkillError(str(exc))
|
|
384
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@skill(
|
|
388
|
+
"web_extract_tables",
|
|
389
|
+
"Extract every table on the page as rows of cells (DOM).",
|
|
390
|
+
PermissionLevel.DESKTOP,
|
|
391
|
+
)
|
|
392
|
+
def web_extract_tables(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
393
|
+
from .browser_extract import get_web_extractor
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
extraction = get_web_extractor().tables()
|
|
397
|
+
except OperatorError as exc:
|
|
398
|
+
raise SkillError(str(exc))
|
|
399
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@skill(
|
|
403
|
+
"web_extract_lists",
|
|
404
|
+
"Extract list structures (ul/ol) with their items.",
|
|
405
|
+
PermissionLevel.DESKTOP,
|
|
406
|
+
)
|
|
407
|
+
def web_extract_lists(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
408
|
+
from .browser_extract import get_web_extractor
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
extraction = get_web_extractor().lists()
|
|
412
|
+
except OperatorError as exc:
|
|
413
|
+
raise SkillError(str(exc))
|
|
414
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@skill(
|
|
418
|
+
"web_extract_metadata",
|
|
419
|
+
"Extract page metadata: description, og tags, canonical url, language.",
|
|
420
|
+
PermissionLevel.DESKTOP,
|
|
421
|
+
)
|
|
422
|
+
def web_extract_metadata(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
423
|
+
from .browser_extract import get_web_extractor
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
extraction = get_web_extractor().metadata()
|
|
427
|
+
except OperatorError as exc:
|
|
428
|
+
raise SkillError(str(exc))
|
|
429
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@skill(
|
|
433
|
+
"web_extract_structured",
|
|
434
|
+
"Extract JSON-LD structured data (products, articles, recipes...).",
|
|
435
|
+
PermissionLevel.DESKTOP,
|
|
436
|
+
)
|
|
437
|
+
def web_extract_structured(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
438
|
+
from .browser_extract import get_web_extractor
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
extraction = get_web_extractor().structured_data()
|
|
442
|
+
except OperatorError as exc:
|
|
443
|
+
raise SkillError(str(exc))
|
|
444
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@skill(
|
|
448
|
+
"web_find",
|
|
449
|
+
"Find where a phrase appears on the page, with surrounding context.",
|
|
450
|
+
PermissionLevel.DESKTOP,
|
|
451
|
+
{"query": "text to locate on the page"},
|
|
452
|
+
)
|
|
453
|
+
def web_find(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
454
|
+
from .browser_extract import get_web_extractor
|
|
455
|
+
|
|
456
|
+
query = str(params.get("query", "")).strip()
|
|
457
|
+
if not query:
|
|
458
|
+
raise SkillError("web_find requires a 'query' parameter")
|
|
459
|
+
try:
|
|
460
|
+
extraction = get_web_extractor().find_on_page(query)
|
|
461
|
+
except OperatorError as exc:
|
|
462
|
+
raise SkillError(str(exc))
|
|
463
|
+
return Outcome(_compact(extraction.to_dict()))
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@skill(
|
|
467
|
+
"web_wait_for",
|
|
468
|
+
"Wait (bounded, state-based) until a CSS selector exists and is visible.",
|
|
469
|
+
PermissionLevel.DESKTOP,
|
|
470
|
+
{"selector": "CSS selector to wait for",
|
|
471
|
+
"timeout": "(optional) seconds (max 30)"},
|
|
472
|
+
)
|
|
473
|
+
def web_wait_for(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
474
|
+
from .browser_extract import get_web_extractor
|
|
475
|
+
|
|
476
|
+
selector = str(params.get("selector", "")).strip()
|
|
477
|
+
if not selector:
|
|
478
|
+
raise SkillError("web_wait_for requires a 'selector' parameter")
|
|
479
|
+
try:
|
|
480
|
+
timeout = float(params.get("timeout", 8.0) or 8.0)
|
|
481
|
+
except (TypeError, ValueError):
|
|
482
|
+
timeout = 8.0
|
|
483
|
+
try:
|
|
484
|
+
ok = get_web_extractor().wait_for_element(selector, timeout_s=timeout)
|
|
485
|
+
except OperatorError as exc:
|
|
486
|
+
raise SkillError(str(exc))
|
|
487
|
+
if not ok:
|
|
488
|
+
raise SkillError(f'"{selector}" did not appear within {timeout:g}s.')
|
|
489
|
+
return Outcome(f'"{selector}" is present and visible')
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# --- memory ----------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
@skill(
|
|
495
|
+
"memory_save",
|
|
496
|
+
"Persist a structured note into local memory (web findings, app usage, "
|
|
497
|
+
"preferences). Secret-like fields are refused.",
|
|
498
|
+
PermissionLevel.WORKSPACE,
|
|
499
|
+
{"namespace": "sessions | desktop | user | web | files",
|
|
500
|
+
"id": "record id, e.g. 'app:spotify'", "summary": "one-line summary",
|
|
501
|
+
"data": "(optional) JSON object to store",
|
|
502
|
+
"tags": "(optional) comma-separated tags"},
|
|
503
|
+
)
|
|
504
|
+
def memory_save(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
505
|
+
from ..core.errors import SecurityError
|
|
506
|
+
from ..memory.store import memory_store
|
|
507
|
+
|
|
508
|
+
namespace = str(params.get("namespace", "")).strip()
|
|
509
|
+
record_id = str(params.get("id", "")).strip()
|
|
510
|
+
summary = str(params.get("summary", "")).strip()
|
|
511
|
+
if not namespace or not record_id or not summary:
|
|
512
|
+
raise SkillError("memory_save requires 'namespace', 'id', and 'summary'")
|
|
513
|
+
data = params.get("data")
|
|
514
|
+
if isinstance(data, str):
|
|
515
|
+
try:
|
|
516
|
+
data = json.loads(data) if data.strip() else {}
|
|
517
|
+
except ValueError:
|
|
518
|
+
raise SkillError("'data' must be a JSON object")
|
|
519
|
+
if data is not None and not isinstance(data, dict):
|
|
520
|
+
raise SkillError("'data' must be a JSON object")
|
|
521
|
+
tags = [t.strip() for t in str(params.get("tags", "")).split(",") if t.strip()]
|
|
522
|
+
try:
|
|
523
|
+
ok = memory_store().put(namespace, record_id, summary, data, tags)
|
|
524
|
+
except SecurityError as exc:
|
|
525
|
+
raise SkillError(str(exc))
|
|
526
|
+
if not ok:
|
|
527
|
+
raise SkillError("memory write failed (disk error)")
|
|
528
|
+
return Outcome(f"saved {namespace}/{record_id}")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@skill(
|
|
532
|
+
"memory_search",
|
|
533
|
+
"Search local memory indexes (compact results; load records with "
|
|
534
|
+
"memory_get). Use for \"what did I...\" questions.",
|
|
535
|
+
PermissionLevel.WORKSPACE,
|
|
536
|
+
{"query": "text to look for", "namespace": "(optional) restrict to one namespace",
|
|
537
|
+
"tags": "(optional) comma-separated tags"},
|
|
538
|
+
)
|
|
539
|
+
def memory_search(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
540
|
+
from ..memory.store import memory_store
|
|
541
|
+
|
|
542
|
+
query = str(params.get("query", "")).strip()
|
|
543
|
+
namespace = str(params.get("namespace", "")).strip()
|
|
544
|
+
tags = [t.strip() for t in str(params.get("tags", "")).split(",") if t.strip()]
|
|
545
|
+
store = memory_store()
|
|
546
|
+
namespaces = [namespace] if namespace else list(store.namespaces()) or ["sessions"]
|
|
547
|
+
results: dict[str, Any] = {}
|
|
548
|
+
for ns in namespaces:
|
|
549
|
+
try:
|
|
550
|
+
hits = store.query(ns, text=query, tags=tags, limit=8)
|
|
551
|
+
except ValueError:
|
|
552
|
+
continue
|
|
553
|
+
if hits:
|
|
554
|
+
results[ns] = hits
|
|
555
|
+
return Outcome(_compact(results) if results else "no matching memory records")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@skill(
|
|
559
|
+
"memory_get",
|
|
560
|
+
"Load one memory record by namespace and id.",
|
|
561
|
+
PermissionLevel.WORKSPACE,
|
|
562
|
+
{"namespace": "the record's namespace", "id": "the record id"},
|
|
563
|
+
)
|
|
564
|
+
def memory_get(ctx: SkillContext, params: dict[str, Any]) -> Outcome:
|
|
565
|
+
from ..memory.store import memory_store
|
|
566
|
+
|
|
567
|
+
namespace = str(params.get("namespace", "")).strip()
|
|
568
|
+
record_id = str(params.get("id", "")).strip()
|
|
569
|
+
if not namespace or not record_id:
|
|
570
|
+
raise SkillError("memory_get requires 'namespace' and 'id'")
|
|
571
|
+
record = memory_store().get(namespace, record_id)
|
|
572
|
+
if record is None:
|
|
573
|
+
raise SkillError(f"no record '{record_id}' in '{namespace}'")
|
|
574
|
+
return Outcome(_compact({
|
|
575
|
+
"id": record.id, "summary": record.summary, "tags": record.tags,
|
|
576
|
+
"created_at": record.created_at, "data": record.data,
|
|
577
|
+
}))
|