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,203 @@
|
|
|
1
|
+
"""Desktop Control permission group: Y[once] / A[always] / N[deny] grants.
|
|
2
|
+
|
|
3
|
+
Filesystem permissions (see :mod:`seedcode.tools.permissions`) bound WHERE the
|
|
4
|
+
agent may act; this module bounds WHETHER it may touch the desktop at all.
|
|
5
|
+
Grants are per-category and session-only — nothing here is persisted, so a
|
|
6
|
+
fresh session always starts with a fresh ask. Sensitive categories (registry
|
|
7
|
+
writes, secrets, purchases, system power, deletions) can never be granted
|
|
8
|
+
"Always": they re-prompt on every single action by design.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Callable
|
|
16
|
+
|
|
17
|
+
from ..tools.permissions import PermissionError_
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DesktopGrant(str, Enum):
|
|
21
|
+
ONCE = "once"
|
|
22
|
+
ALWAYS = "always"
|
|
23
|
+
DENY = "deny"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Categories an action may fall into. `control` covers plain input and
|
|
27
|
+
# observation; the rest gate riskier ground.
|
|
28
|
+
CATEGORY_CONTROL = "control" # mouse, keyboard, screenshots, window reads
|
|
29
|
+
CATEGORY_APPS = "apps" # opening / closing applications
|
|
30
|
+
CATEGORY_REGISTRY_READ = "registry_read"
|
|
31
|
+
CATEGORY_NETWORK = "network" # downloads / non-navigation web transfers
|
|
32
|
+
|
|
33
|
+
# Sensitive categories: confirmation can never be remembered ("Always" is
|
|
34
|
+
# treated as "Once"), so each individual action is user-approved.
|
|
35
|
+
CATEGORY_REGISTRY_WRITE = "registry_write"
|
|
36
|
+
CATEGORY_SECRET = "type_secret" # typing passwords / credentials
|
|
37
|
+
CATEGORY_SYSTEM = "system" # shutdown, restart, system settings
|
|
38
|
+
CATEGORY_DELETE = "delete" # deleting files via the desktop
|
|
39
|
+
CATEGORY_PURCHASE = "purchase" # browser purchases / payments
|
|
40
|
+
CATEGORY_INSTALL = "install" # installing software (never remembered)
|
|
41
|
+
|
|
42
|
+
SENSITIVE_CATEGORIES = frozenset(
|
|
43
|
+
{
|
|
44
|
+
CATEGORY_REGISTRY_WRITE,
|
|
45
|
+
CATEGORY_SECRET,
|
|
46
|
+
CATEGORY_SYSTEM,
|
|
47
|
+
CATEGORY_DELETE,
|
|
48
|
+
CATEGORY_PURCHASE,
|
|
49
|
+
CATEGORY_INSTALL,
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# The non-sensitive categories Assist Mode grants up-front in ONE prompt when it
|
|
54
|
+
# starts, so the session never re-asks for routine control during a task. These
|
|
55
|
+
# are exactly the categories a normal automation session touches; sensitive
|
|
56
|
+
# categories are deliberately excluded — they always confirm per action.
|
|
57
|
+
SESSION_GRANT_CATEGORIES: tuple[str, ...] = (
|
|
58
|
+
CATEGORY_CONTROL,
|
|
59
|
+
CATEGORY_APPS,
|
|
60
|
+
CATEGORY_REGISTRY_READ,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
CATEGORY_LABELS = {
|
|
64
|
+
CATEGORY_CONTROL: "Desktop control (mouse, keyboard, screen)",
|
|
65
|
+
CATEGORY_APPS: "Open / close applications",
|
|
66
|
+
CATEGORY_REGISTRY_READ: "Read the Windows registry",
|
|
67
|
+
CATEGORY_NETWORK: "Download files from the web",
|
|
68
|
+
CATEGORY_REGISTRY_WRITE: "WRITE to the Windows registry",
|
|
69
|
+
CATEGORY_SECRET: "Type a password or secret",
|
|
70
|
+
CATEGORY_SYSTEM: "System action (shutdown / restart / settings)",
|
|
71
|
+
CATEGORY_DELETE: "Delete files via the desktop",
|
|
72
|
+
CATEGORY_PURCHASE: "Browser purchase / payment",
|
|
73
|
+
CATEGORY_INSTALL: "Install software",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# confirm(category, description) -> the user's choice for this ask.
|
|
77
|
+
ConfirmCallback = Callable[[str, str], DesktopGrant]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _deny_all(category: str, description: str) -> DesktopGrant:
|
|
81
|
+
"""Default callback when no UI is wired: refuse everything."""
|
|
82
|
+
return DesktopGrant.DENY
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class SessionPermissionManager:
|
|
87
|
+
"""Session-wide desktop grants, requested ONCE when Assist Mode starts.
|
|
88
|
+
|
|
89
|
+
This is the fix for "asks for permission on every action": instead of
|
|
90
|
+
prompting lazily the first time each category is touched, Assist Mode calls
|
|
91
|
+
:meth:`request` at start-up to grant the whole routine-control set in a
|
|
92
|
+
single dialog. Every :class:`DesktopSession` created for the session
|
|
93
|
+
(the app rebuilds them whenever the permission level changes) consults the
|
|
94
|
+
*same* manager, so a grant made once holds for the entire session and
|
|
95
|
+
survives those rebuilds.
|
|
96
|
+
|
|
97
|
+
Only re-prompt when an action *exceeds* what was granted — i.e. sensitive
|
|
98
|
+
categories, which are never blanket-granted here and always confirm per
|
|
99
|
+
action. Nothing is persisted to disk; a new process starts fresh.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
granted: set[str] = field(default_factory=set)
|
|
103
|
+
denied: set[str] = field(default_factory=set)
|
|
104
|
+
requested: bool = False
|
|
105
|
+
|
|
106
|
+
def request(self, categories: "list[str] | tuple[str, ...]", allow: bool) -> None:
|
|
107
|
+
"""Record the user's one-time answer for a batch of categories."""
|
|
108
|
+
self.requested = True
|
|
109
|
+
for category in categories:
|
|
110
|
+
if category in SENSITIVE_CATEGORIES:
|
|
111
|
+
continue # sensitive is never granted for the whole session
|
|
112
|
+
if allow:
|
|
113
|
+
self.granted.add(category)
|
|
114
|
+
self.denied.discard(category)
|
|
115
|
+
else:
|
|
116
|
+
self.denied.add(category)
|
|
117
|
+
self.granted.discard(category)
|
|
118
|
+
|
|
119
|
+
def is_granted(self, category: str) -> bool:
|
|
120
|
+
return category in self.granted
|
|
121
|
+
|
|
122
|
+
def is_denied(self, category: str) -> bool:
|
|
123
|
+
return category in self.denied
|
|
124
|
+
|
|
125
|
+
def reset(self) -> None:
|
|
126
|
+
"""Forget the session decision (Assist Mode turned off)."""
|
|
127
|
+
self.granted.clear()
|
|
128
|
+
self.denied.clear()
|
|
129
|
+
self.requested = False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# One manager per process, shared by every DesktopSession the app builds.
|
|
133
|
+
_SESSION = SessionPermissionManager()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def session_permissions() -> SessionPermissionManager:
|
|
137
|
+
"""The process-wide :class:`SessionPermissionManager`."""
|
|
138
|
+
return _SESSION
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class DesktopSession:
|
|
143
|
+
"""Session-scoped Desktop Control gate consulted by every desktop tool.
|
|
144
|
+
|
|
145
|
+
``enabled`` mirrors ``config.desktop_mode``; ``confirm`` is wired to the
|
|
146
|
+
UI prompt by the app layer. :meth:`check` either returns silently or
|
|
147
|
+
raises :class:`PermissionError_` with text the model can act on.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
enabled: bool = False
|
|
151
|
+
confirm: ConfirmCallback = _deny_all
|
|
152
|
+
grants: dict[str, DesktopGrant] = field(default_factory=dict)
|
|
153
|
+
# Base64 PNG screenshots queued by observation tools; the agent loop
|
|
154
|
+
# drains this and attaches images when the provider supports vision.
|
|
155
|
+
pending_images: list[str] = field(default_factory=list)
|
|
156
|
+
# Session-wide grants requested once at Assist start-up. When wired by the
|
|
157
|
+
# app layer, a category the user already approved for the session is never
|
|
158
|
+
# re-prompted here. None (the default, and in unit tests) preserves the
|
|
159
|
+
# original lazy per-category behaviour exactly.
|
|
160
|
+
session: "SessionPermissionManager | None" = None
|
|
161
|
+
|
|
162
|
+
def check(self, category: str, description: str) -> None:
|
|
163
|
+
"""Gate one desktop action; raises PermissionError_ when refused."""
|
|
164
|
+
if not self.enabled:
|
|
165
|
+
raise PermissionError_(
|
|
166
|
+
"Blocked: desktop mode is off. The user can enable it with /desktop on."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
sensitive = category in SENSITIVE_CATEGORIES
|
|
170
|
+
if not sensitive:
|
|
171
|
+
# Session-wide decision (asked once at /assist on) takes priority.
|
|
172
|
+
if self.session is not None:
|
|
173
|
+
if self.session.is_granted(category):
|
|
174
|
+
return
|
|
175
|
+
if self.session.is_denied(category):
|
|
176
|
+
raise PermissionError_(
|
|
177
|
+
f"Blocked: the user denied "
|
|
178
|
+
f"'{CATEGORY_LABELS.get(category, category)}' for this session."
|
|
179
|
+
)
|
|
180
|
+
granted = self.grants.get(category)
|
|
181
|
+
if granted is DesktopGrant.ALWAYS:
|
|
182
|
+
return
|
|
183
|
+
if granted is DesktopGrant.DENY:
|
|
184
|
+
raise PermissionError_(
|
|
185
|
+
f"Blocked: the user denied '{CATEGORY_LABELS.get(category, category)}' "
|
|
186
|
+
"for this session."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
choice = self.confirm(category, description)
|
|
190
|
+
if sensitive and choice is DesktopGrant.ALWAYS:
|
|
191
|
+
# Sensitive actions are approved one at a time, never blanket.
|
|
192
|
+
choice = DesktopGrant.ONCE
|
|
193
|
+
if not sensitive and choice in (DesktopGrant.ALWAYS, DesktopGrant.DENY):
|
|
194
|
+
self.grants[category] = choice
|
|
195
|
+
if choice is DesktopGrant.DENY:
|
|
196
|
+
raise PermissionError_(
|
|
197
|
+
f"Blocked: the user denied this action ({description})."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def reset(self) -> None:
|
|
201
|
+
"""Forget all session grants (used when desktop mode toggles)."""
|
|
202
|
+
self.grants.clear()
|
|
203
|
+
self.pending_images.clear()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Recovery engine: deterministic reflexes before the AI is asked to replan.
|
|
2
|
+
|
|
3
|
+
When an action fails verification, the dispatcher hands the situation to the
|
|
4
|
+
recovery engine, which walks a fixed ladder of local strategies — retry,
|
|
5
|
+
refocus the target window, dismiss a stray dialog, re-snapshot the UI, restart
|
|
6
|
+
the app — re-running the action after each. Only when every strategy is
|
|
7
|
+
exhausted does control return to the dispatcher, which then (and only then)
|
|
8
|
+
asks the AI planner to replan.
|
|
9
|
+
|
|
10
|
+
The engine is deterministic and offline: it composes existing controller
|
|
11
|
+
primitives and never calls an AI provider. Strategies are ordered from
|
|
12
|
+
cheapest/least disruptive to most disruptive so a transient hiccup is fixed
|
|
13
|
+
without, say, killing and relaunching an application.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Callable
|
|
20
|
+
|
|
21
|
+
# The result of trying to recover: whether the action ultimately succeeded and
|
|
22
|
+
# the trail of strategies attempted (surfaced in logs and to the AI on replan).
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class RecoveryOutcome:
|
|
25
|
+
recovered: bool
|
|
26
|
+
detail: str
|
|
27
|
+
strategies_tried: list[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# An action is a zero-arg callable that performs the operation and returns a
|
|
31
|
+
# short result string; it raises on hard failure. A verify is a zero-arg
|
|
32
|
+
# callable returning a truthy VerifyResult-like object.
|
|
33
|
+
Action = Callable[[], str]
|
|
34
|
+
Verify = Callable[[], Any]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RecoveryEngine:
|
|
38
|
+
"""Applies local recovery strategies to a failed, verified action."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, controller: Any = None, state: Any = None) -> None:
|
|
41
|
+
self._controller = controller
|
|
42
|
+
self._state = state
|
|
43
|
+
|
|
44
|
+
def recover(
|
|
45
|
+
self,
|
|
46
|
+
action: Action,
|
|
47
|
+
verify: Verify,
|
|
48
|
+
*,
|
|
49
|
+
window_title: str | None = None,
|
|
50
|
+
app_target: str | None = None,
|
|
51
|
+
) -> RecoveryOutcome:
|
|
52
|
+
"""Try to make ``action`` verify, walking the strategy ladder.
|
|
53
|
+
|
|
54
|
+
Each strategy nudges the environment, re-runs the action, then
|
|
55
|
+
re-verifies. Returns as soon as verification passes.
|
|
56
|
+
"""
|
|
57
|
+
tried: list[str] = []
|
|
58
|
+
last = "no recovery strategies applied"
|
|
59
|
+
for name, strategy in self._ladder(window_title, app_target):
|
|
60
|
+
tried.append(name)
|
|
61
|
+
try:
|
|
62
|
+
strategy()
|
|
63
|
+
except Exception:
|
|
64
|
+
# A strategy that itself fails is not fatal — move to the next.
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
action()
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
last = f"retry after {name} raised: {exc}"
|
|
70
|
+
continue
|
|
71
|
+
result = verify()
|
|
72
|
+
if result:
|
|
73
|
+
return RecoveryOutcome(
|
|
74
|
+
True, f"recovered via {name}: {getattr(result, 'detail', 'ok')}", tried
|
|
75
|
+
)
|
|
76
|
+
last = f"still failing after {name}: {getattr(result, 'detail', result)}"
|
|
77
|
+
return RecoveryOutcome(False, last, tried)
|
|
78
|
+
|
|
79
|
+
def _ladder(
|
|
80
|
+
self, window_title: str | None, app_target: str | None
|
|
81
|
+
) -> list[tuple[str, Callable[[], None]]]:
|
|
82
|
+
"""Ordered (name, strategy) pairs, cheapest and least disruptive first."""
|
|
83
|
+
c = self._controller
|
|
84
|
+
ladder: list[tuple[str, Callable[[], None]]] = []
|
|
85
|
+
|
|
86
|
+
# 1) Plain retry: let transient timing settle.
|
|
87
|
+
ladder.append(("retry", lambda: c.wait(0.5) if c else None))
|
|
88
|
+
|
|
89
|
+
# 2) Dismiss a stray modal/tooltip that may be intercepting input.
|
|
90
|
+
ladder.append(("dismiss_dialog", lambda: c.hotkey(["esc"]) if c else None))
|
|
91
|
+
|
|
92
|
+
# 3) Refocus the intended window — the most common real cause.
|
|
93
|
+
if window_title and c:
|
|
94
|
+
ladder.append(("refocus_window", lambda: c.focus_window(window_title)))
|
|
95
|
+
|
|
96
|
+
# 4) Re-snapshot the UI so the next resolve sees the current tree.
|
|
97
|
+
if c:
|
|
98
|
+
ladder.append(("resnapshot", lambda: c.see(window_title)))
|
|
99
|
+
|
|
100
|
+
# 5) Restart the app: close then reopen, the most disruptive step.
|
|
101
|
+
if app_target and c:
|
|
102
|
+
def _restart() -> None:
|
|
103
|
+
try:
|
|
104
|
+
c.close_app(app_target, force=False)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
c.wait(0.5)
|
|
108
|
+
c.open_app(app_target)
|
|
109
|
+
if window_title:
|
|
110
|
+
c.wait(0.5)
|
|
111
|
+
c.focus_window(window_title)
|
|
112
|
+
|
|
113
|
+
ladder.append(("restart_app", _restart))
|
|
114
|
+
|
|
115
|
+
return ladder
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Registry driver: read anywhere, write only through explicit confirmation.
|
|
2
|
+
|
|
3
|
+
The permission layer (:mod:`seedcode.computer.permissions`) forces a per-write
|
|
4
|
+
user confirmation — this module only performs the operations. Uses the
|
|
5
|
+
standard-library ``winreg``; hive names accept the common long and short
|
|
6
|
+
forms (HKEY_CURRENT_USER / HKCU, ...).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
_HIVE_ALIASES = {
|
|
14
|
+
"hkcu": "HKEY_CURRENT_USER",
|
|
15
|
+
"hkey_current_user": "HKEY_CURRENT_USER",
|
|
16
|
+
"hklm": "HKEY_LOCAL_MACHINE",
|
|
17
|
+
"hkey_local_machine": "HKEY_LOCAL_MACHINE",
|
|
18
|
+
"hkcr": "HKEY_CLASSES_ROOT",
|
|
19
|
+
"hkey_classes_root": "HKEY_CLASSES_ROOT",
|
|
20
|
+
"hku": "HKEY_USERS",
|
|
21
|
+
"hkey_users": "HKEY_USERS",
|
|
22
|
+
"hkcc": "HKEY_CURRENT_CONFIG",
|
|
23
|
+
"hkey_current_config": "HKEY_CURRENT_CONFIG",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_MAX_LIST = 200
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _winreg():
|
|
30
|
+
if sys.platform != "win32":
|
|
31
|
+
raise RuntimeError("The registry only exists on Windows.")
|
|
32
|
+
import winreg
|
|
33
|
+
|
|
34
|
+
return winreg
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_key(path: str):
|
|
38
|
+
"""Split 'HKCU\\Software\\...' into (hive handle, subkey, display name)."""
|
|
39
|
+
winreg = _winreg()
|
|
40
|
+
raw = (path or "").strip().strip("\\")
|
|
41
|
+
if not raw:
|
|
42
|
+
raise ValueError("Registry path is required, e.g. HKCU\\Software\\MyApp.")
|
|
43
|
+
hive_part, _, subkey = raw.partition("\\")
|
|
44
|
+
hive_name = _HIVE_ALIASES.get(hive_part.lower())
|
|
45
|
+
if hive_name is None:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Unknown registry hive '{hive_part}'. "
|
|
48
|
+
"Use HKCU, HKLM, HKCR, HKU, or HKCC."
|
|
49
|
+
)
|
|
50
|
+
return getattr(winreg, hive_name), subkey, f"{hive_name}\\{subkey}".rstrip("\\")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_value(path: str, name: str) -> str:
|
|
54
|
+
"""Read one value ('' name means the key's default value)."""
|
|
55
|
+
winreg = _winreg()
|
|
56
|
+
hive, subkey, display = parse_key(path)
|
|
57
|
+
with winreg.OpenKey(hive, subkey) as key:
|
|
58
|
+
value, kind = winreg.QueryValueEx(key, name or "")
|
|
59
|
+
return f"{display} : {name or '(default)'} = {value!r} (type {kind})"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_key(path: str) -> str:
|
|
63
|
+
"""List a key's subkeys and values."""
|
|
64
|
+
winreg = _winreg()
|
|
65
|
+
hive, subkey, display = parse_key(path)
|
|
66
|
+
subkeys: list[str] = []
|
|
67
|
+
values: list[str] = []
|
|
68
|
+
with winreg.OpenKey(hive, subkey) as key:
|
|
69
|
+
info = winreg.QueryInfoKey(key)
|
|
70
|
+
for i in range(min(info[0], _MAX_LIST)):
|
|
71
|
+
subkeys.append(winreg.EnumKey(key, i))
|
|
72
|
+
for i in range(min(info[1], _MAX_LIST)):
|
|
73
|
+
name, value, kind = winreg.EnumValue(key, i)
|
|
74
|
+
values.append(f"{name or '(default)'} = {value!r} (type {kind})")
|
|
75
|
+
lines = [f"{display}:"]
|
|
76
|
+
if subkeys:
|
|
77
|
+
lines.append(" subkeys: " + ", ".join(subkeys))
|
|
78
|
+
if values:
|
|
79
|
+
lines += [f" {v}" for v in values]
|
|
80
|
+
if not subkeys and not values:
|
|
81
|
+
lines.append(" (empty)")
|
|
82
|
+
return "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def write_value(path: str, name: str, value: str, value_type: str = "REG_SZ") -> str:
|
|
86
|
+
"""Create/overwrite a string or dword value (key is created if missing)."""
|
|
87
|
+
winreg = _winreg()
|
|
88
|
+
hive, subkey, display = parse_key(path)
|
|
89
|
+
kind_name = value_type.strip().upper()
|
|
90
|
+
if kind_name == "REG_SZ":
|
|
91
|
+
kind, data = winreg.REG_SZ, str(value)
|
|
92
|
+
elif kind_name == "REG_DWORD":
|
|
93
|
+
kind, data = winreg.REG_DWORD, int(value)
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError("Only REG_SZ and REG_DWORD writes are supported.")
|
|
96
|
+
with winreg.CreateKey(hive, subkey) as key:
|
|
97
|
+
winreg.SetValueEx(key, name or "", 0, kind, data)
|
|
98
|
+
return f"Wrote {display} : {name or '(default)'} = {data!r} ({kind_name})"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def delete_value(path: str, name: str) -> str:
|
|
102
|
+
"""Delete one value from a key."""
|
|
103
|
+
winreg = _winreg()
|
|
104
|
+
hive, subkey, display = parse_key(path)
|
|
105
|
+
with winreg.OpenKey(hive, subkey, 0, winreg.KEY_SET_VALUE) as key:
|
|
106
|
+
winreg.DeleteValue(key, name or "")
|
|
107
|
+
return f"Deleted {display} : {name or '(default)'}"
|