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,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)'}"