cmdop 0.1.28__py3-none-any.whl → 0.1.30__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.
cmdop/__init__.py CHANGED
@@ -120,6 +120,11 @@ from cmdop.helpers import (
120
120
  NetworkAnalyzer,
121
121
  NetworkSnapshot,
122
122
  RequestSnapshot,
123
+ # Desktop management
124
+ ensure_desktop_running,
125
+ start_desktop,
126
+ handle_cmdop_error,
127
+ with_auto_restart,
123
128
  )
124
129
  from cmdop.logging import (
125
130
  get_logger,
@@ -128,7 +133,7 @@ from cmdop.logging import (
128
133
  get_log_dir,
129
134
  )
130
135
 
131
- __version__ = "0.1.28"
136
+ __version__ = "0.1.30"
132
137
 
133
138
  __all__ = [
134
139
  # Version
@@ -225,6 +230,11 @@ __all__ = [
225
230
  "NetworkAnalyzer",
226
231
  "NetworkSnapshot",
227
232
  "RequestSnapshot",
233
+ # Desktop management
234
+ "ensure_desktop_running",
235
+ "start_desktop",
236
+ "handle_cmdop_error",
237
+ "with_auto_restart",
228
238
  # Logging
229
239
  "get_logger",
230
240
  "setup_logging",
cmdop/helpers/__init__.py CHANGED
@@ -5,9 +5,23 @@ from cmdop.helpers.network_analyzer import (
5
5
  NetworkSnapshot,
6
6
  RequestSnapshot,
7
7
  )
8
+ from cmdop.helpers.desktop import (
9
+ ensure_desktop_running,
10
+ start_desktop,
11
+ handle_cmdop_error,
12
+ with_auto_restart,
13
+ get_cmdop_app_path,
14
+ )
8
15
 
9
16
  __all__ = [
17
+ # Network
10
18
  "NetworkAnalyzer",
11
19
  "NetworkSnapshot",
12
20
  "RequestSnapshot",
21
+ # Desktop management
22
+ "ensure_desktop_running",
23
+ "start_desktop",
24
+ "handle_cmdop_error",
25
+ "with_auto_restart",
26
+ "get_cmdop_app_path",
13
27
  ]
@@ -0,0 +1,350 @@
1
+ """
2
+ CMDOP Desktop management helpers.
3
+
4
+ Cross-platform utilities for starting, stopping, and ensuring
5
+ CMDOP Desktop is running.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import platform
11
+ import subprocess
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Callable, TypeVar
16
+
17
+ from cmdop.exceptions import AgentNotRunningError, StalePortFileError
18
+ from cmdop.transport.discovery import discover_agent, cleanup_stale_discovery
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ # =============================================================================
24
+ # Platform-specific paths
25
+ # =============================================================================
26
+
27
+ def get_cmdop_app_path() -> Path | None:
28
+ """Get platform-specific path to CMDOP Desktop app."""
29
+ system = platform.system()
30
+
31
+ if system == "Darwin":
32
+ path = Path("/Applications/CMDOP.app")
33
+ if path.exists():
34
+ return path
35
+ # Check user Applications
36
+ user_path = Path.home() / "Applications" / "CMDOP.app"
37
+ if user_path.exists():
38
+ return user_path
39
+
40
+ elif system == "Windows":
41
+ # Common Windows install locations
42
+ paths = [
43
+ Path(r"C:\Program Files\CMDOP\CMDOP.exe"),
44
+ Path(r"C:\Program Files (x86)\CMDOP\CMDOP.exe"),
45
+ Path.home() / "AppData" / "Local" / "Programs" / "CMDOP" / "CMDOP.exe",
46
+ ]
47
+ for path in paths:
48
+ if path.exists():
49
+ return path
50
+
51
+ elif system == "Linux":
52
+ # Linux: check if cmdop binary is available
53
+ try:
54
+ result = subprocess.run(
55
+ ["which", "cmdop"],
56
+ capture_output=True,
57
+ text=True,
58
+ )
59
+ if result.returncode == 0:
60
+ return Path(result.stdout.strip())
61
+ except Exception:
62
+ pass
63
+
64
+ return None
65
+
66
+
67
+ # =============================================================================
68
+ # Core functions
69
+ # =============================================================================
70
+
71
+ def start_desktop(timeout: float = 15.0, quiet: bool = False) -> bool:
72
+ """
73
+ Start CMDOP Desktop application.
74
+
75
+ Cross-platform function that starts CMDOP Desktop and waits
76
+ for the agent to become available.
77
+
78
+ Args:
79
+ timeout: Maximum time to wait for agent to start (seconds)
80
+ quiet: If True, suppress output messages
81
+
82
+ Returns:
83
+ True if agent started successfully, False otherwise
84
+ """
85
+ system = platform.system()
86
+
87
+ def log(msg: str) -> None:
88
+ if not quiet:
89
+ print(msg)
90
+
91
+ log("🔄 Starting CMDOP Desktop...")
92
+
93
+ try:
94
+ if system == "Darwin":
95
+ app_path = get_cmdop_app_path()
96
+ if app_path:
97
+ subprocess.Popen(
98
+ ["open", str(app_path)],
99
+ stdout=subprocess.DEVNULL,
100
+ stderr=subprocess.DEVNULL,
101
+ )
102
+ else:
103
+ log("❌ CMDOP.app not found in /Applications")
104
+ return False
105
+
106
+ elif system == "Windows":
107
+ app_path = get_cmdop_app_path()
108
+ # DETACHED_PROCESS = 0x00000008
109
+ detached_process = 0x00000008
110
+ if app_path:
111
+ subprocess.Popen(
112
+ [str(app_path)],
113
+ stdout=subprocess.DEVNULL,
114
+ stderr=subprocess.DEVNULL,
115
+ creationflags=detached_process,
116
+ )
117
+ else:
118
+ # Try start command as fallback
119
+ subprocess.Popen(
120
+ ["cmd", "/c", "start", "", "CMDOP"],
121
+ stdout=subprocess.DEVNULL,
122
+ stderr=subprocess.DEVNULL,
123
+ shell=True,
124
+ )
125
+
126
+ elif system == "Linux":
127
+ # Linux: start cmdop serve in background
128
+ subprocess.Popen(
129
+ ["cmdop", "serve"],
130
+ stdout=subprocess.DEVNULL,
131
+ stderr=subprocess.DEVNULL,
132
+ start_new_session=True,
133
+ )
134
+
135
+ else:
136
+ log(f"❌ Unsupported platform: {system}")
137
+ return False
138
+
139
+ except Exception as e:
140
+ log(f"❌ Failed to start CMDOP: {e}")
141
+ return False
142
+
143
+ log("⏳ Waiting for CMDOP to initialize...")
144
+
145
+ # Poll until agent is available
146
+ start_time = time.time()
147
+ last_dot_time = start_time
148
+
149
+ while time.time() - start_time < timeout:
150
+ result = discover_agent(verify_alive=True)
151
+ if result.found:
152
+ log("✅ CMDOP Desktop started successfully")
153
+ return True
154
+
155
+ # Print dots to show progress
156
+ if not quiet and time.time() - last_dot_time >= 1.0:
157
+ print(".", end="", flush=True)
158
+ last_dot_time = time.time()
159
+
160
+ time.sleep(0.3)
161
+
162
+ if not quiet:
163
+ print() # Newline after dots
164
+ log(f"❌ CMDOP did not start within {timeout}s")
165
+ return False
166
+
167
+
168
+ def ensure_desktop_running(
169
+ timeout: float = 15.0,
170
+ auto_start: bool = True,
171
+ exit_on_failure: bool = True,
172
+ ) -> bool:
173
+ """
174
+ Ensure CMDOP Desktop is running, starting it if needed.
175
+
176
+ This is the main function to use before browser operations.
177
+ Handles stale discovery files automatically.
178
+
179
+ Args:
180
+ timeout: Time to wait for agent if starting
181
+ auto_start: If True, attempt to start CMDOP if not running
182
+ exit_on_failure: If True, call sys.exit(1) on failure
183
+
184
+ Returns:
185
+ True if CMDOP is running
186
+
187
+ Raises:
188
+ SystemExit: If exit_on_failure=True and CMDOP cannot be started
189
+
190
+ Example:
191
+ >>> from cmdop.helpers import ensure_desktop_running
192
+ >>> ensure_desktop_running() # Starts CMDOP if needed
193
+ >>> # Now safe to use browser
194
+ >>> client = CMDOPClient.local()
195
+ """
196
+ result = discover_agent(verify_alive=True)
197
+
198
+ if result.found:
199
+ return True
200
+
201
+ # Handle stale discovery file
202
+ if result.discovery_path:
203
+ print(f"⚠️ Cleaning stale discovery file: {result.discovery_path}")
204
+ cleanup_stale_discovery(result.discovery_path)
205
+
206
+ print("⚠️ CMDOP Desktop is not running")
207
+
208
+ if not auto_start:
209
+ if exit_on_failure:
210
+ print("\nStart CMDOP Desktop manually:")
211
+ _print_start_instructions()
212
+ sys.exit(1)
213
+ return False
214
+
215
+ # Try to start
216
+ if start_desktop(timeout=timeout):
217
+ return True
218
+
219
+ if exit_on_failure:
220
+ print("\n💡 Try starting CMDOP Desktop manually:")
221
+ _print_start_instructions()
222
+ sys.exit(1)
223
+
224
+ return False
225
+
226
+
227
+ def handle_cmdop_error(
228
+ error: Exception,
229
+ auto_restart: bool = True,
230
+ exit_on_failure: bool = True,
231
+ ) -> bool:
232
+ """
233
+ Handle CMDOP connection errors with auto-restart.
234
+
235
+ Use this in except blocks to handle AgentNotRunningError
236
+ and StalePortFileError gracefully.
237
+
238
+ Args:
239
+ error: The exception that was raised
240
+ auto_restart: If True, attempt to restart CMDOP
241
+ exit_on_failure: If True, call sys.exit(1) if restart fails
242
+
243
+ Returns:
244
+ True if CMDOP was restarted successfully
245
+
246
+ Example:
247
+ >>> try:
248
+ ... client = CMDOPClient.local()
249
+ ... session = client.browser.create_session()
250
+ ... except (StalePortFileError, AgentNotRunningError) as e:
251
+ ... if handle_cmdop_error(e):
252
+ ... # Retry the operation
253
+ ... client = CMDOPClient.local()
254
+ """
255
+ if isinstance(error, StalePortFileError):
256
+ print(f"\n⚠️ CMDOP Desktop crashed (stale file: {error.discovery_path})")
257
+ error.cleanup()
258
+ elif isinstance(error, AgentNotRunningError):
259
+ print("\n⚠️ CMDOP Desktop is not running")
260
+ else:
261
+ # Unknown error, re-raise
262
+ raise error
263
+
264
+ if not auto_restart:
265
+ if exit_on_failure:
266
+ print("\nStart CMDOP Desktop:")
267
+ _print_start_instructions()
268
+ sys.exit(1)
269
+ return False
270
+
271
+ if start_desktop():
272
+ return True
273
+
274
+ if exit_on_failure:
275
+ print("\n💡 Try starting CMDOP Desktop manually:")
276
+ _print_start_instructions()
277
+ sys.exit(1)
278
+
279
+ return False
280
+
281
+
282
+ def with_auto_restart(
283
+ func: Callable[..., T],
284
+ *args,
285
+ max_retries: int = 1,
286
+ **kwargs,
287
+ ) -> T:
288
+ """
289
+ Execute function with automatic CMDOP restart on failure.
290
+
291
+ Wraps a function that uses CMDOP and automatically handles
292
+ connection errors by restarting CMDOP Desktop.
293
+
294
+ Args:
295
+ func: Function to execute
296
+ *args: Arguments to pass to function
297
+ max_retries: Maximum restart attempts
298
+ **kwargs: Keyword arguments to pass to function
299
+
300
+ Returns:
301
+ Result of the function
302
+
303
+ Example:
304
+ >>> def parse_page():
305
+ ... client = CMDOPClient.local()
306
+ ... with client.browser.create_session() as session:
307
+ ... session.navigate("https://example.com")
308
+ ... return session.get_text("body")
309
+ ...
310
+ >>> result = with_auto_restart(parse_page)
311
+ """
312
+ last_error: Exception | None = None
313
+
314
+ for attempt in range(max_retries + 1):
315
+ try:
316
+ return func(*args, **kwargs)
317
+ except (StalePortFileError, AgentNotRunningError) as e:
318
+ last_error = e
319
+
320
+ if attempt < max_retries:
321
+ print(f"\n🔄 CMDOP error (attempt {attempt + 1}/{max_retries + 1})")
322
+ if handle_cmdop_error(e, auto_restart=True, exit_on_failure=False):
323
+ continue
324
+
325
+ # Last attempt failed
326
+ handle_cmdop_error(e, auto_restart=False, exit_on_failure=True)
327
+
328
+ # Should not reach here, but just in case
329
+ if last_error:
330
+ raise last_error
331
+ raise RuntimeError("Unexpected error in with_auto_restart")
332
+
333
+
334
+ # =============================================================================
335
+ # Private helpers
336
+ # =============================================================================
337
+
338
+ def _print_start_instructions() -> None:
339
+ """Print platform-specific start instructions."""
340
+ system = platform.system()
341
+
342
+ if system == "Darwin":
343
+ print(" open /Applications/CMDOP.app")
344
+ elif system == "Windows":
345
+ print(" Start CMDOP from Start Menu")
346
+ print(" or run: CMDOP.exe")
347
+ elif system == "Linux":
348
+ print(" cmdop serve")
349
+ else:
350
+ print(" Start CMDOP Desktop application")
cmdop/models/config.py CHANGED
@@ -16,8 +16,8 @@ class KeepaliveConfig(BaseModel):
16
16
 
17
17
  model_config = ConfigDict(extra="forbid", frozen=True)
18
18
 
19
- time_ms: Annotated[int, Field(ge=1000, le=300_000)] = 10_000
20
- """Interval between keepalive pings (ms). Default: 10s"""
19
+ time_ms: Annotated[int, Field(ge=1000, le=300_000)] = 30_000
20
+ """Interval between keepalive pings (ms). Default: 30s"""
21
21
 
22
22
  timeout_ms: Annotated[int, Field(ge=500, le=60_000)] = 5_000
23
23
  """Timeout waiting for ping response (ms). Default: 5s"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmdop
3
- Version: 0.1.28
3
+ Version: 0.1.30
4
4
  Summary: Python SDK for CMDOP agent interaction
5
5
  Project-URL: Homepage, https://cmdop.com
6
6
  Project-URL: Documentation, https://cmdop.com
@@ -1,4 +1,4 @@
1
- cmdop/__init__.py,sha256=ICgs-pVllaoCC_UzbiGoDalx1ZdVYLvbAXdLmNALLUk,5271
1
+ cmdop/__init__.py,sha256=FCoVmVkm-3kuPc9kyU5mH4tac503r1IoP0kqmqERi7g,5517
2
2
  cmdop/client.py,sha256=nTotStZPBfYN3TrHH-OlEJMSVAXskYMQRkocsFmyaBY,14601
3
3
  cmdop/config.py,sha256=vpw1aGCyS4NKlZyzVur81Lt06QmN3FnscZji0bypUi0,4398
4
4
  cmdop/discovery.py,sha256=HNxSOa5tSuG7ppfFs21XdviW5ucjpRswVPguhX5j8Dg,7479
@@ -150,12 +150,13 @@ cmdop/api/generated/workspaces/workspaces__api__workspaces/__init__.py,sha256=Wo
150
150
  cmdop/api/generated/workspaces/workspaces__api__workspaces/client.py,sha256=RcBOR3WCicf5xSjsZApJ9oQ3a0PGpZD6z-2R0IbxRHQ,16547
151
151
  cmdop/api/generated/workspaces/workspaces__api__workspaces/models.py,sha256=KCmK-CUuODGIqREu3WBlKKic9SElk7MY3OkD8zNVS78,10230
152
152
  cmdop/api/generated/workspaces/workspaces__api__workspaces/sync_client.py,sha256=BKpMmMYzGw6pmDItC_TpJ_mvuI1zmDppXObWncWszE4,16147
153
- cmdop/helpers/__init__.py,sha256=EGoWhmQojGlwic4oISx7pwFq5WDSfdNh9hhTxzwdkbs,220
153
+ cmdop/helpers/__init__.py,sha256=1kSurulAO8E_uG-E2RK3zGlRCmBLpDmPfuVH8cz7Gpo,543
154
+ cmdop/helpers/desktop.py,sha256=v3E-a54dB6cV7NEFUivM7C0Xko1uEMMpGWfWA-uHxtg,10350
154
155
  cmdop/helpers/network_analyzer.py,sha256=ZiTRv39S_kTAppTVbq97DYLExS2WsDLCxf_N4rtnkf0,13025
155
156
  cmdop/models/__init__.py,sha256=W6P1oo6JkUAeVEV59HzFT646hXM0pk_obXHfHbX4tAc,1594
156
157
  cmdop/models/agent.py,sha256=Z1QDfr1-DTFVl5oPvbH2ZUBLXPHUlitiCRfG7y_OzYg,5495
157
158
  cmdop/models/base.py,sha256=1SR1ka5p-rHHkk4k9pPwbraxX_CsTG830CosGNPn1JA,7425
158
- cmdop/models/config.py,sha256=i3Pc7i57dYqryuIjkigmjlUWpR3lz9Obxwyu5Xa8Lzc,3198
159
+ cmdop/models/config.py,sha256=kBkr4TFBxM07JMBG349D00C_2pzW7RGzxeGAoRCFwZw,3198
159
160
  cmdop/models/extract.py,sha256=-HOoAYZ8c7mNNOVOfW3cR6NrhzMvJla5jiGbphsfOBs,2098
160
161
  cmdop/models/files.py,sha256=Up96os9fjbDNsVt_wYZKOs3kc-i4uPEOr2KWtItKRYc,3528
161
162
  cmdop/models/terminal.py,sha256=g5xcNEa_d3Dvq13QGHRBdh-lkFoN1MU_O1QNmyFM7nI,5135
@@ -198,7 +199,7 @@ cmdop/transport/base.py,sha256=2pkV8i9epgp_21dyReCfX47abRUrnALm0W5BXb-Fuz0,5571
198
199
  cmdop/transport/discovery.py,sha256=rcGAuVrR1l6jwcP0dqZxVhX1NsFK7sRHygFMCLmmUbA,10673
199
200
  cmdop/transport/local.py,sha256=ob6tWVxSdKwblHSMK8CkgjyuSdQoAeWgy5OAUd5ZNuE,7411
200
201
  cmdop/transport/remote.py,sha256=FNVqus9wOv7LlxKarXjLmSyvJiHwhvPbNDOPv1IQkmE,4329
201
- cmdop-0.1.28.dist-info/METADATA,sha256=DQ-6-awSrNB7dwy_j_7cfj6tsNIAl-zogYNTd3WVJbQ,10563
202
- cmdop-0.1.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
203
- cmdop-0.1.28.dist-info/licenses/LICENSE,sha256=6hyzbI1QVXW6B-XT7PaQ6UG9lns11Y_nnap8uUKGUqo,1062
204
- cmdop-0.1.28.dist-info/RECORD,,
202
+ cmdop-0.1.30.dist-info/METADATA,sha256=hhryMuocXskH1_EwQsX3Cclay-3n_E_eCJOuPMYGvkI,10563
203
+ cmdop-0.1.30.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
204
+ cmdop-0.1.30.dist-info/licenses/LICENSE,sha256=6hyzbI1QVXW6B-XT7PaQ6UG9lns11Y_nnap8uUKGUqo,1062
205
+ cmdop-0.1.30.dist-info/RECORD,,
File without changes