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,241 @@
1
+ """Application discovery across trusted Windows sources.
2
+
3
+ The ladder, cheapest and most trustworthy first (all injectable):
4
+
5
+ 1. **Start Menu shortcuts** (``.lnk``) — the canonical user-visible app list;
6
+ both per-user and all-users Start Menus are scanned.
7
+ 2. **App Paths registry** — per-machine/per-user registered executables
8
+ (``HKLM\\...\\App Paths\\spotify.exe``).
9
+ 3. **Uninstall registrations** — installed-app metadata (name, install
10
+ location, display icon), the same source "Add/Remove Programs" reads.
11
+ 4. **PATH lookup** — ``shutil.which`` for CLI tools and portable apps.
12
+
13
+ Every source is matched case-insensitively on the requested name; the
14
+ requester gets one :class:`AppInfo` with the safest available launch target.
15
+ Non-Windows platforms return "not found" rather than guessing.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import shutil
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from ..core.errors import ApplicationNotFoundError
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class AppInfo:
31
+ """One discovered application."""
32
+
33
+ name: str
34
+ source: str # start_menu | app_paths | uninstall | path
35
+ target: str = "" # shortcut path or executable
36
+ exe: str = "" # resolved executable path ("" when unknown)
37
+ install_location: str = ""
38
+ pid: int = 0 # set by the launcher/verifier, not discovery
39
+ matched: str = "" # which discovery key matched
40
+
41
+ def describe(self) -> str:
42
+ via = f" via {self.source}" if self.source else ""
43
+ return f'"{self.name}"{via}'
44
+
45
+
46
+ # Directory-name fragments for the two Start Menu program folders.
47
+ _START_MENU_DIRS = (
48
+ Path(os.environ.get("APPDATA", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs",
49
+ Path(os.environ.get("PROGRAMDATA", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs",
50
+ )
51
+
52
+
53
+ def _iter_start_menu() -> list[Path]:
54
+ shortcuts: list[Path] = []
55
+ for base in _START_MENU_DIRS:
56
+ try:
57
+ if base.is_dir():
58
+ shortcuts.extend(base.rglob("*.lnk"))
59
+ except OSError:
60
+ continue
61
+ return shortcuts
62
+
63
+
64
+ def _shortcut_display_name(path: Path) -> str:
65
+ """A .lnk file's user-visible name: its stem (no resolution needed)."""
66
+ return path.stem.strip()
67
+
68
+
69
+ def _match_name(haystack: str, needle: str) -> bool:
70
+ """Case-insensitive whole-word-ish containment match."""
71
+ h = " ".join(haystack.lower().split())
72
+ n = " ".join(needle.lower().split())
73
+ return bool(n) and (n == h or n in h)
74
+
75
+
76
+ # --- source 2: App Paths -----------------------------------------------------------
77
+ def _app_paths() -> dict[str, AppInfo]:
78
+ """``App Paths`` registrations: exe name -> full path (best-effort)."""
79
+ found: dict[str, AppInfo] = {}
80
+ if os.name != "nt":
81
+ return found
82
+ try:
83
+ import winreg
84
+
85
+ for hive, flag in ((winreg.HKEY_LOCAL_MACHINE, 0), (winreg.HKEY_CURRENT_USER, 0)):
86
+ try:
87
+ root = winreg.OpenKey(hive, r"Software\Microsoft\Windows\CurrentVersion\App Paths", 0, winreg.KEY_READ | flag)
88
+ except OSError:
89
+ continue
90
+ with root:
91
+ i = 0
92
+ while True:
93
+ try:
94
+ subkey_name = winreg.EnumKey(root, i)
95
+ except OSError:
96
+ break
97
+ i += 1
98
+ try:
99
+ with winreg.OpenKey(root, subkey_name) as sub:
100
+ exe_path, _t = winreg.QueryValueEx(sub, "")
101
+ except OSError:
102
+ continue
103
+ name = Path(subkey_name).stem
104
+ found[name.lower()] = AppInfo(
105
+ name=name, source="app_paths",
106
+ target=str(exe_path or ""), exe=str(exe_path or ""),
107
+ matched=subkey_name,
108
+ )
109
+ except ImportError:
110
+ pass
111
+ return found
112
+
113
+
114
+ # --- source 3: uninstall registrations ----------------------------------------------
115
+ def _uninstall_apps() -> dict[str, AppInfo]:
116
+ """Display-name/install-location entries from the uninstall keys."""
117
+ found: dict[str, AppInfo] = {}
118
+ if os.name != "nt":
119
+ return found
120
+ try:
121
+ import winreg
122
+
123
+ paths = (
124
+ r"Software\Microsoft\Windows\CurrentVersion\Uninstall",
125
+ r"Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
126
+ )
127
+ for hive in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
128
+ for subpath in paths:
129
+ try:
130
+ root = winreg.OpenKey(hive, subpath, 0, winreg.KEY_READ)
131
+ except OSError:
132
+ continue
133
+ with root:
134
+ i = 0
135
+ while True:
136
+ try:
137
+ subkey_name = winreg.EnumKey(root, i)
138
+ except OSError:
139
+ break
140
+ i += 1
141
+ try:
142
+ with winreg.OpenKey(root, subkey_name) as sub:
143
+ display, _t = winreg.QueryValueEx(sub, "DisplayName")
144
+ try:
145
+ loc, _t2 = winreg.QueryValueEx(sub, "InstallLocation")
146
+ except OSError:
147
+ loc = ""
148
+ except OSError:
149
+ continue
150
+ name = str(display or "").strip()
151
+ if not name:
152
+ continue
153
+ entry = AppInfo(
154
+ name=name, source="uninstall",
155
+ install_location=str(loc or ""),
156
+ matched=subkey_name,
157
+ )
158
+ found.setdefault(name.lower(), entry)
159
+ except ImportError:
160
+ pass
161
+ return found
162
+
163
+
164
+ # --- the discovery facade -------------------------------------------------------------
165
+ def find_app(name: str, *, start_menu: list[Path] | None = None) -> AppInfo:
166
+ """Locate an installed application by (fuzzy) name.
167
+
168
+ ``start_menu`` injects the shortcut list for tests. Raises
169
+ :class:`ApplicationNotFoundError` when every source misses.
170
+ """
171
+ wanted = (name or "").strip()
172
+ if not wanted:
173
+ raise ApplicationNotFoundError("An application name is required.")
174
+
175
+ # 1) Start Menu shortcuts — the canonical list. Prefer exact stem matches.
176
+ shortcuts = _iter_start_menu() if start_menu is None else start_menu
177
+ exact: Path | None = None
178
+ partial: Path | None = None
179
+ for path in shortcuts:
180
+ display = _shortcut_display_name(path)
181
+ if display.lower() == wanted.lower():
182
+ exact = path
183
+ break
184
+ if partial is None and _match_name(display, wanted):
185
+ partial = path
186
+ chosen = exact or partial
187
+ if chosen is not None:
188
+ return AppInfo(
189
+ name=_shortcut_display_name(chosen), source="start_menu",
190
+ target=str(chosen), matched=str(chosen),
191
+ )
192
+
193
+ # 2) App Paths (spotify.exe -> full path).
194
+ for app_name, info in _app_paths().items():
195
+ if _match_name(app_name, wanted):
196
+ return info
197
+
198
+ # 3) Uninstall registrations (covers apps without Start Menu entries).
199
+ for app_name, info in _uninstall_apps().items():
200
+ if _match_name(app_name, wanted):
201
+ # Uninstall entries know the location but not the exe; a launch
202
+ # attempt will resolve the target from the install location.
203
+ if info.install_location:
204
+ loc = Path(info.install_location)
205
+ for candidate in (wanted.lower(), wanted.lower().replace(" ", "")):
206
+ exe = loc / f"{candidate}.exe"
207
+ if exe.is_file():
208
+ info.exe = str(exe)
209
+ info.target = str(exe)
210
+ break
211
+ return info
212
+
213
+ # 4) PATH — CLI tools and portable apps.
214
+ which = shutil.which(wanted)
215
+ if which:
216
+ return AppInfo(
217
+ name=wanted, source="path", target=which, exe=which, matched=which
218
+ )
219
+
220
+ raise ApplicationNotFoundError(
221
+ f'"{wanted}" is not installed (searched Start Menu, App Paths, '
222
+ "installed-app registrations, and PATH)."
223
+ )
224
+
225
+
226
+ def installed_apps(*, start_menu: list[Path] | None = None) -> list[AppInfo]:
227
+ """Every discoverable app (Start Menu + App Paths), name-deduplicated.
228
+
229
+ Used by memory and by "what can I open?" queries; not for matching.
230
+ """
231
+ seen: dict[str, AppInfo] = {}
232
+ shortcuts = _iter_start_menu() if start_menu is None else start_menu
233
+ for path in shortcuts:
234
+ display = _shortcut_display_name(path)
235
+ if display and display.lower() not in seen:
236
+ seen[display.lower()] = AppInfo(
237
+ name=display, source="start_menu", target=str(path), matched=str(path)
238
+ )
239
+ for key, info in _app_paths().items():
240
+ seen.setdefault(key, info)
241
+ return sorted(seen.values(), key=lambda a: a.name.lower())
@@ -0,0 +1,164 @@
1
+ """Permission-gated application installation (missing-app workflow).
2
+
3
+ When a requested application is not installed, the flow is:
4
+
5
+ missing app → resolve a trusted source → SHOW the user exactly what will
6
+ be installed and from where → explicit confirmation (INSTALL category,
7
+ never remembered) → download/execute → verify installed → launch.
8
+
9
+ Security properties:
10
+
11
+ * The INSTALL category is **sensitive**: "always allow" is impossible by
12
+ construction (the permission layer downgrades it), so every install asks.
13
+ * Only **trusted sources** are eligible: Winget (preferred), Microsoft
14
+ Store, or a caller-supplied official vendor URL. Arbitrary URLs the model
15
+ produces are refused by :func:`_trusted_source`.
16
+ * The model cannot mark anything trusted; trust comes from the winget
17
+ manifest registry or a user-supplied URL, never from model output.
18
+ * Installers are executed only after the user confirms the exact source
19
+ shown to them.
20
+
21
+ The actual winget invocation is injectable so tests never touch a system.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import shutil
27
+ import subprocess
28
+ from dataclasses import dataclass
29
+ from typing import Any
30
+
31
+ from ..core.errors import ApplicationNotFoundError, SecurityError
32
+ from .discovery import AppInfo
33
+
34
+ # Seconds before a winget invocation is declared wedged.
35
+ _WINGET_TIMEOUT_S = 600
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class InstallPlan:
40
+ """What would be installed, shown to the user before confirmation."""
41
+
42
+ app_name: str
43
+ mechanism: str # winget | store | manual
44
+ package_id: str = "" # winget package id, when known
45
+ source: str = "" # human-readable source description
46
+ detail: str = ""
47
+
48
+ def describe(self) -> str:
49
+ lines = [f"Install {self.app_name} via {self.mechanism}"]
50
+ if self.package_id:
51
+ lines.append(f" package: {self.package_id}")
52
+ if self.source:
53
+ lines.append(f" source: {self.source}")
54
+ return "\n".join(lines)
55
+
56
+
57
+ @dataclass(slots=True)
58
+ class InstallResult:
59
+ success: bool
60
+ app_name: str
61
+ detail: str = ""
62
+
63
+
64
+ # --- trusted source resolution ------------------------------------------------------
65
+ def _winget_package(app_name: str, run: Any = None) -> InstallPlan | None:
66
+ """A winget plan for ``app_name``, or None when winget can't find one."""
67
+ if shutil.which("winget") is None:
68
+ return None
69
+ if run is None:
70
+ def run(args: list[str]) -> subprocess.CompletedProcess:
71
+ return subprocess.run( # noqa: S603 — fixed argv, no shell
72
+ args, capture_output=True, text=True, timeout=60
73
+ )
74
+ try:
75
+ search = run(["winget", "search", "--id", app_name, "--accept-source-agreements"])
76
+ out = (search.stdout or "") + (search.stderr or "")
77
+ # winget prints a table; any non-error row mentioning the name counts.
78
+ if search.returncode == 0 and app_name.lower() in out.lower():
79
+ return InstallPlan(
80
+ app_name=app_name, mechanism="winget", package_id=app_name,
81
+ source="winget (curated package registry)",
82
+ detail=f"winget found a package matching '{app_name}'",
83
+ )
84
+ except Exception:
85
+ return None
86
+ return None
87
+
88
+
89
+ def resolve_install_plan(app_name: str, *, run: Any = None) -> InstallPlan:
90
+ """A trusted install plan for a missing app, or an error.
91
+
92
+ Only winget is currently wired; Microsoft Store routes through winget on
93
+ modern Windows. If nothing trusted exists, the user is told to install
94
+ it manually — SeedCode never fetches an arbitrary installer URL.
95
+ """
96
+ plan = _winget_package(app_name, run=run)
97
+ if plan is not None:
98
+ return plan
99
+ raise SecurityError(
100
+ f'No trusted installation source found for "{app_name}". SeedCode '
101
+ "will not download installers from arbitrary websites. Install it "
102
+ "manually (official site or Microsoft Store), then ask me to open it."
103
+ )
104
+
105
+
106
+ def install_app(
107
+ app_name: str,
108
+ *,
109
+ confirm: Any,
110
+ run: Any = None,
111
+ find: Any = None,
112
+ launch: Any = None,
113
+ ) -> InstallResult:
114
+ """The guarded install workflow. ``confirm(plan_description) -> bool``.
115
+
116
+ ``confirm`` is wired to the interactive permission dialog by the caller
117
+ (skill/tools layer); it is asked AFTER the plan (mechanism + package +
118
+ source) is shown and is the only path to executing anything.
119
+ """
120
+ from ..core.errors import OperatorError
121
+
122
+ try:
123
+ plan = resolve_install_plan(app_name, run=run)
124
+ except OperatorError:
125
+ raise
126
+
127
+ if not confirm(plan.describe()):
128
+ return InstallResult(
129
+ False, app_name, "installation declined by the user"
130
+ )
131
+
132
+ # Execute via winget with a fixed, audited argv.
133
+ if run is None:
134
+ def run(args: list[str], **kw: Any) -> subprocess.CompletedProcess:
135
+ return subprocess.run( # noqa: S603 — fixed argv, no shell
136
+ args, capture_output=True, text=True, timeout=_WINGET_TIMEOUT_S
137
+ )
138
+ try:
139
+ proc = run([
140
+ "winget", "install", "--id", plan.package_id,
141
+ "--accept-source-agreements", "--accept-package-agreements",
142
+ ])
143
+ except Exception as exc:
144
+ return InstallResult(False, app_name, f"installer failed to run: {exc}")
145
+ ok = getattr(proc, "returncode", 1) == 0
146
+ if not ok:
147
+ tail = ((getattr(proc, "stderr", "") or "") or (getattr(proc, "stdout", "") or "")).strip()
148
+ return InstallResult(False, app_name, f"installer returned an error: {tail[:300]}")
149
+
150
+ # Verify installation actually happened (discovery must now find it).
151
+ if find is None:
152
+ from .discovery import find_app as find
153
+ try:
154
+ find(app_name)
155
+ except ApplicationNotFoundError:
156
+ return InstallResult(
157
+ False, app_name,
158
+ "installer reported success but the app is not discoverable yet; "
159
+ "it may need a moment or a fresh Start Menu index",
160
+ )
161
+ return InstallResult(True, app_name, f"installed {app_name} via {plan.mechanism}")
162
+
163
+
164
+ __all__ = ["InstallPlan", "InstallResult", "resolve_install_plan", "install_app"]
@@ -0,0 +1,156 @@
1
+ """Application launching with state-based waiting and duplicate avoidance.
2
+
3
+ The launch workflow never spawns blindly:
4
+
5
+ 1. **Running?** Match open windows against the app name (via the windows
6
+ driver, which already excludes SeedCode's own console). A visible window
7
+ means *focus the existing instance* instead of launching a duplicate.
8
+ 2. **Launch** through the safest target: a Start Menu shortcut
9
+ (``os.startfile`` resolves it), then a resolved executable, then the
10
+ shell ``start`` fallback — never a guessed bare ``subprocess`` call.
11
+ 3. **Wait for state, not for a timer:** poll for a matching window/process
12
+ up to ``MAX_WAIT_WINDOW_S`` instead of a fixed sleep.
13
+ 4. **Verify** and return a structured result; the caller (skill) reports
14
+ success only when evidence exists.
15
+
16
+ All OS interaction is injectable for tests.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import subprocess
23
+ import time
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ from ..core.errors import ApplicationLaunchError, ApplicationNotFoundError
28
+ from ..core.limits import MAX_WAIT_WINDOW_S, MAX_WAIT_POLL_S, clamp_wait
29
+ from .discovery import AppInfo, find_app
30
+ from .verifier import app_running, wait_for_app_window
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class LaunchResult:
35
+ """Structured outcome of one open-app attempt."""
36
+
37
+ success: bool
38
+ application: str
39
+ focused_existing: bool = False
40
+ window_detected: bool = False
41
+ window_title: str = ""
42
+ pid: int = 0
43
+ detail: str = ""
44
+
45
+ def describe(self) -> str:
46
+ return self.detail or (f"{self.application}: "
47
+ + ("focused existing window" if self.focused_existing
48
+ else "launched")
49
+ + (" (window verified)" if self.window_detected else ""))
50
+
51
+
52
+ def focus_existing(app: AppInfo, windows_driver: Any = None) -> LaunchResult | None:
53
+ """Focus a running instance if one exists; None when not running.
54
+
55
+ Matching uses the window list (titles already exclude our console) and,
56
+ where available, the owning process name — never a blind title guess.
57
+ """
58
+ if windows_driver is None:
59
+ from ..computer import windows as windows_driver # type: ignore
60
+ needle = app.name.lower()
61
+ try:
62
+ wins = windows_driver.list_windows()
63
+ except Exception:
64
+ return None
65
+ for w in wins:
66
+ title = (getattr(w, "title", "") or "").lower()
67
+ if needle in title:
68
+ try:
69
+ windows_driver.focus_window(w.title)
70
+ except Exception:
71
+ pass # focus is best-effort; window evidence stands
72
+ return LaunchResult(
73
+ success=True, application=app.name, focused_existing=True,
74
+ window_detected=True, window_title=getattr(w, "title", ""),
75
+ pid=int(getattr(w, "pid", 0) or 0),
76
+ detail=f'focused existing window "{getattr(w, "title", "")}"',
77
+ )
78
+ return None
79
+
80
+
81
+ def launch_app(
82
+ name: str,
83
+ *,
84
+ find: Any = find_app,
85
+ windows_driver: Any = None,
86
+ startfile: Any = None,
87
+ popen: Any = None,
88
+ wait_s: float = MAX_WAIT_WINDOW_S,
89
+ ) -> LaunchResult:
90
+ """Open an application: reuse → launch → wait → verify.
91
+
92
+ Injectable parameters exist purely for tests; production callers pass
93
+ only ``name``.
94
+ """
95
+ if startfile is None:
96
+ def startfile(target: str) -> None:
97
+ os.startfile(target) # noqa: S606 — shell-resolved launch
98
+ if popen is None:
99
+ def popen(argv: list[str]) -> subprocess.Popen:
100
+ return subprocess.Popen( # noqa: S603 — resolved target only
101
+ argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
102
+ stdin=subprocess.DEVNULL,
103
+ )
104
+
105
+ try:
106
+ app = find(name)
107
+ except ApplicationNotFoundError:
108
+ raise # the caller (skill) turns this into the install-permission flow
109
+
110
+ # Step 1: already running? Focus, don't duplicate.
111
+ existing = focus_existing(app, windows_driver)
112
+ if existing is not None:
113
+ return existing
114
+
115
+ # Step 2: launch via the safest target.
116
+ target = app.target or app.exe
117
+ if not target:
118
+ raise ApplicationLaunchError(
119
+ f'"{app.name}" is installed but exposes no launchable target.'
120
+ )
121
+ launched_via = "shortcut" if app.source == "start_menu" else "executable"
122
+ try:
123
+ if app.source == "start_menu" or target.lower().endswith(".lnk"):
124
+ startfile(target)
125
+ else:
126
+ popen([target])
127
+ except OSError as exc:
128
+ raise ApplicationLaunchError(f'Could not launch "{app.name}": {exc}')
129
+
130
+ # Step 3+4: state-based wait for a real window (no blind sleep).
131
+ window = wait_for_app_window(app, windows_driver, timeout_s=wait_s)
132
+ if window is not None:
133
+ return LaunchResult(
134
+ success=True, application=app.name, window_detected=True,
135
+ window_title=window, pid=app.pid,
136
+ detail=f'launched {app.name} via {launched_via}; window "{window}" detected',
137
+ )
138
+ # No window yet: accept a running process as weaker evidence.
139
+ pid = app_running(app, windows_driver)
140
+ if pid:
141
+ return LaunchResult(
142
+ success=True, application=app.name, window_detected=False,
143
+ pid=pid, detail=f"launched {app.name} (process running, no window yet)",
144
+ )
145
+ return LaunchResult(
146
+ success=False, application=app.name,
147
+ detail=f"launched {app.name} but no window or process evidence within {wait_s:g}s",
148
+ )
149
+
150
+
151
+ def open_or_raise(name: str, **kw: Any) -> LaunchResult:
152
+ """Convenience wrapper: find+launch, translating discovery misses."""
153
+ return launch_app(name, **kw)
154
+
155
+
156
+ __all__ = ["LaunchResult", "launch_app", "focus_existing", "open_or_raise"]
@@ -0,0 +1,119 @@
1
+ """Application launch verification: evidence, not assumptions.
2
+
3
+ ``launch`` alone proves nothing on Windows — the shell accepts the request
4
+ and the app may crash a second later. This module checks for *evidence*
5
+ that the application actually came up:
6
+
7
+ * a window whose title matches the app (strong evidence), or
8
+ * a running process whose name matches (weaker evidence), via the window
9
+ list's owning pids (psutil when present, best-effort).
10
+
11
+ ``wait_for_app_window`` polls — it never sleeps blindly — and returns the
12
+ matched window title, or None after the bounded timeout.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ from typing import Any
19
+
20
+ from ..core.limits import MAX_WAIT_WINDOW_S, MAX_WAIT_POLL_S, clamp_wait
21
+ from .discovery import AppInfo
22
+
23
+
24
+ def _process_names() -> set[int]:
25
+ """Pids of running processes whose name plausibly matches apps (best-effort)."""
26
+ pids: set[int] = set()
27
+ try:
28
+ import psutil # type: ignore
29
+
30
+ for proc in psutil.process_iter(["pid"]):
31
+ try:
32
+ pids.add(int(proc.info["pid"]))
33
+ except Exception:
34
+ continue
35
+ except Exception:
36
+ pass
37
+ return pids
38
+
39
+
40
+ def _window_fragments(app: AppInfo) -> list[str]:
41
+ """Title fragments that identify this app's windows."""
42
+ name = (app.name or "").lower().strip()
43
+ fragments = [name] if name else []
44
+ exe = (app.exe or "")
45
+ if exe:
46
+ stem = exe.replace("\\", "/").rsplit("/", 1)[-1]
47
+ stem = stem[:-4] if stem.lower().endswith(".exe") else stem
48
+ if stem:
49
+ fragments.append(stem.lower())
50
+ return fragments
51
+
52
+
53
+ def find_app_window(app: AppInfo, windows_driver: Any = None) -> str | None:
54
+ """The title of a window belonging to ``app``, or None."""
55
+ if windows_driver is None:
56
+ from ..computer import windows as windows_driver # type: ignore
57
+ try:
58
+ wins = windows_driver.list_windows()
59
+ except Exception:
60
+ return None
61
+ fragments = _window_fragments(app)
62
+ # Prefer process-id equality when the window carries one.
63
+ for w in wins:
64
+ if app.pid and int(getattr(w, "pid", 0) or 0) == app.pid:
65
+ return getattr(w, "title", "")
66
+ for w in wins:
67
+ title = (getattr(w, "title", "") or "").lower()
68
+ if any(f and f in title for f in fragments):
69
+ return getattr(w, "title", "")
70
+ return None
71
+
72
+
73
+ def app_running(app: AppInfo, windows_driver: Any = None) -> int:
74
+ """The app's running pid (from windows/processes), or 0."""
75
+ if windows_driver is None:
76
+ from ..computer import windows as windows_driver # type: ignore
77
+ try:
78
+ wins = windows_driver.list_windows()
79
+ except Exception:
80
+ wins = []
81
+ fragments = _window_fragments(app)
82
+ for w in wins:
83
+ title = (getattr(w, "title", "") or "").lower()
84
+ if any(f and f in title for f in fragments):
85
+ pid = int(getattr(w, "pid", 0) or 0)
86
+ if pid:
87
+ return pid
88
+ # No window: consult live processes by exe stem.
89
+ live = _process_names()
90
+ if not live:
91
+ return 0
92
+ try:
93
+ import psutil # type: ignore
94
+
95
+ exe_stem = (app.exe or "").replace("\\", "/").rsplit("/", 1)[-1].lower()
96
+ for proc in psutil.process_iter(["pid", "name"]):
97
+ name = (proc.info.get("name") or "").lower()
98
+ if exe_stem and name == exe_stem:
99
+ return int(proc.info["pid"])
100
+ except Exception:
101
+ pass
102
+ return 0
103
+
104
+
105
+ def wait_for_app_window(
106
+ app: AppInfo, windows_driver: Any = None, timeout_s: float = MAX_WAIT_WINDOW_S
107
+ ) -> str | None:
108
+ """Poll for the app's window until it appears or the timeout elapses."""
109
+ deadline = time.monotonic() + clamp_wait(timeout_s, MAX_WAIT_WINDOW_S)
110
+ while True:
111
+ title = find_app_window(app, windows_driver)
112
+ if title:
113
+ return title
114
+ if time.monotonic() >= deadline:
115
+ return None
116
+ time.sleep(MAX_WAIT_POLL_S)
117
+
118
+
119
+ __all__ = ["find_app_window", "app_running", "wait_for_app_window"]
@@ -0,0 +1,15 @@
1
+ ███████╗███████╗███████╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗
2
+ ██╔════╝██╔════╝██╔════╝██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
3
+ ███████╗█████╗ █████╗ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗
4
+ ╚════██║██╔══╝ ██╔══╝ ██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝
5
+ ███████║███████╗███████╗██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗
6
+ ╚══════╝╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
7
+
8
+ S E E D C O D E
9
+
10
+ Plant ideas. Grow code.
11
+
12
+ Created by
13
+ Al Shahriar Sowan
14
+
15
+ Vibe coded with GPT-5o & Claude Opus 4.8