claude-comms 1.0.46 → 2.0.0
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.
- package/CHANGELOG.md +20 -1
- package/README.md +12 -13
- package/package.json +6 -10
- package/.agents/AGENTS.md +0 -61
- package/.agents/repo.md +0 -18
- package/.agents/tools/README.md +0 -45
- package/.agents/tools/agent-job/README.md +0 -208
- package/.agents/tools/agent-job/__pycache__/agent_monitor.cpython-312.pyc +0 -0
- package/.agents/tools/agent-job/agent_job.py +0 -936
- package/.agents/tools/agent-job/agent_monitor.py +0 -575
- package/.agents/tools/agent-job/consultant_template.txt +0 -73
- package/.agents/tools/chrome-devtools/README.md +0 -101
- package/.agents/tools/chrome-devtools/browsertools.py +0 -839
- package/.agents/tools/chrome-devtools/config.json +0 -24
- package/.agents/tools/workflow/CONSUMER_SETUP.md +0 -114
- package/.agents/tools/workflow/ONBOARD.md +0 -167
- package/.agents/tools/workflow/README.md +0 -169
- package/.agents/tools/workflow/cli.ts +0 -726
- package/.agents/tools/workflow/config.example.json +0 -16
- package/.agents/tools/workflow/init.ts +0 -76
- package/.agents/tools/workflow/lib/file-tracker.test.ts +0 -81
- package/.agents/tools/workflow/lib/file-tracker.ts +0 -387
- package/.agents/tools/workflow/lib/harness-executor.ts +0 -649
- package/.agents/tools/workflow/lib/prompts.scenario.test.ts +0 -262
- package/.agents/tools/workflow/lib/prompts.ts +0 -548
- package/.agents/tools/workflow/lib/streams.test.ts +0 -140
- package/.agents/tools/workflow/lib/streams.ts +0 -254
- package/.agents/tools/workflow/runner.ts +0 -994
- package/.agents/tools/workflow/templates/document.md +0 -42
- package/.agents/tools/workflow/templates/implement.md +0 -117
- package/.agents/tools/workflow/templates/plan.md +0 -54
- package/.agents/tools/workflow/templates/pm.md +0 -84
- package/.agents/tools/workflow/templates/product-owner.md +0 -211
- package/.agents/tools/workflow/templates/review.md +0 -56
- package/.agents/tools/workflow/templates/uat.md +0 -71
- package/.claude/agents/core/engineer.md +0 -146
- package/.claude/commands/action-feedback-item.md +0 -27
- package/.claude/commands/install-feedback-widget.md +0 -599
- package/.claude/commands/learn.md +0 -32
- package/.claude/hooks/comms/__pycache__/register_subagent.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/__pycache__/update_subagent_completion.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/get_unread_messages.py +0 -71
- package/.claude/hooks/comms/register_subagent.py +0 -74
- package/.claude/hooks/comms/send_message.py +0 -87
- package/.claude/hooks/comms/update_subagent_completion.py +0 -126
- package/.claude/hooks/context/session_id.py +0 -35
- package/.claude/hooks/observability/send_event.py +0 -110
- package/.claude/hooks/session-data/get_session_data.py +0 -261
- package/.claude/hooks/session-data/intercept_session_id.py +0 -69
- package/.claude/hooks/utils/__pycache__/server_config.cpython-312.pyc +0 -0
- package/.claude/hooks/utils/server_config.py +0 -17
- package/.claude/settings.json +0 -108
- package/.claude/settings.local.json +0 -46
- package/CLAUDE.md +0 -240
|
@@ -1,839 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S uv run --quiet --script
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.11"
|
|
4
|
-
# dependencies = [
|
|
5
|
-
# "mcp>=1.1.2",
|
|
6
|
-
# ]
|
|
7
|
-
# ///
|
|
8
|
-
"""
|
|
9
|
-
Chrome DevTools MCP wrapper - persistent daemon with Unix socket bridge.
|
|
10
|
-
|
|
11
|
-
Architecture:
|
|
12
|
-
- Daemon spawns chrome-devtools-mcp once, keeps it running
|
|
13
|
-
- Daemon listens on Unix socket for client commands
|
|
14
|
-
- Multiple CLI invocations connect via socket, share same MCP session
|
|
15
|
-
- State (snapshots, tabs, etc.) persists across all commands
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import asyncio
|
|
19
|
-
import argparse
|
|
20
|
-
import json
|
|
21
|
-
import os
|
|
22
|
-
import signal
|
|
23
|
-
import sys
|
|
24
|
-
import time
|
|
25
|
-
import uuid
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Dict, Optional
|
|
28
|
-
|
|
29
|
-
# State directory
|
|
30
|
-
STATE_DIR = Path.home() / ".browsertools"
|
|
31
|
-
CONFIG_FILE = STATE_DIR / "config.json"
|
|
32
|
-
INACTIVITY_TIMEOUT = 600 # 10 minutes
|
|
33
|
-
|
|
34
|
-
# Global daemon state
|
|
35
|
-
mcp_proc = None
|
|
36
|
-
mcp_reader = None
|
|
37
|
-
mcp_writer = None
|
|
38
|
-
pending_requests = {} # msg_id -> response_future
|
|
39
|
-
last_activity_time = time.time()
|
|
40
|
-
current_instance_id = None
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def generate_instance_id() -> str:
|
|
44
|
-
"""Generate 8-char hex instance ID."""
|
|
45
|
-
return uuid.uuid4().hex[:8]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def get_socket_path(instance_id: str) -> Path:
|
|
49
|
-
"""Get socket path for instance."""
|
|
50
|
-
return STATE_DIR / f"daemon-{instance_id}.sock"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_pid_file(instance_id: str) -> Path:
|
|
54
|
-
"""Get PID file path for instance."""
|
|
55
|
-
return STATE_DIR / f"daemon-{instance_id}.pid"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def ensure_state_dir():
|
|
59
|
-
"""Create state directory."""
|
|
60
|
-
STATE_DIR.mkdir(exist_ok=True)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def get_daemon_pid(instance_id: str) -> Optional[int]:
|
|
64
|
-
"""Get PID of running daemon for instance."""
|
|
65
|
-
pid_file = get_pid_file(instance_id)
|
|
66
|
-
if not pid_file.exists():
|
|
67
|
-
return None
|
|
68
|
-
try:
|
|
69
|
-
pid = int(pid_file.read_text().strip())
|
|
70
|
-
os.kill(pid, 0) # Check if alive
|
|
71
|
-
return pid
|
|
72
|
-
except (ProcessLookupError, ValueError):
|
|
73
|
-
pid_file.unlink(missing_ok=True)
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def is_daemon_running(instance_id: str) -> bool:
|
|
78
|
-
"""Check if daemon is running for instance."""
|
|
79
|
-
return get_daemon_pid(instance_id) is not None and get_socket_path(instance_id).exists()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def get_all_instances() -> list[str]:
|
|
83
|
-
"""Get list of all running instance IDs."""
|
|
84
|
-
instances = []
|
|
85
|
-
if not STATE_DIR.exists():
|
|
86
|
-
return instances
|
|
87
|
-
for pid_file in STATE_DIR.glob("daemon-*.pid"):
|
|
88
|
-
instance_id = pid_file.stem.replace("daemon-", "")
|
|
89
|
-
if is_daemon_running(instance_id):
|
|
90
|
-
instances.append(instance_id)
|
|
91
|
-
else:
|
|
92
|
-
# Clean up stale files
|
|
93
|
-
pid_file.unlink(missing_ok=True)
|
|
94
|
-
get_socket_path(instance_id).unlink(missing_ok=True)
|
|
95
|
-
return instances
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def load_config() -> Dict[str, Any]:
|
|
99
|
-
"""Load config file or return defaults."""
|
|
100
|
-
# Try user config first
|
|
101
|
-
if CONFIG_FILE.exists():
|
|
102
|
-
try:
|
|
103
|
-
with open(CONFIG_FILE) as f:
|
|
104
|
-
return json.load(f)
|
|
105
|
-
except Exception as e:
|
|
106
|
-
print(f"Warning: Failed to load user config: {e}", file=sys.stderr)
|
|
107
|
-
|
|
108
|
-
# Try repo default config
|
|
109
|
-
repo_config = Path(__file__).parent / "config.json"
|
|
110
|
-
if repo_config.exists():
|
|
111
|
-
try:
|
|
112
|
-
with open(repo_config) as f:
|
|
113
|
-
return json.load(f)
|
|
114
|
-
except Exception as e:
|
|
115
|
-
print(f"Warning: Failed to load repo config: {e}", file=sys.stderr)
|
|
116
|
-
|
|
117
|
-
# Fallback to hardcoded defaults
|
|
118
|
-
return {
|
|
119
|
-
"mcp_command": "npx",
|
|
120
|
-
"mcp_args": ["-y", "chrome-devtools-mcp@latest", "--isolated", "--headless=true"]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def save_default_config():
|
|
125
|
-
"""Save default config file."""
|
|
126
|
-
ensure_state_dir()
|
|
127
|
-
config = {
|
|
128
|
-
"mcp_command": "npx",
|
|
129
|
-
"mcp_args": ["-y", "chrome-devtools-mcp@latest", "--isolated"],
|
|
130
|
-
"_comment": "Customize MCP server startup. Examples:",
|
|
131
|
-
"_example_headless": {
|
|
132
|
-
"mcp_args": ["-y", "chrome-devtools-mcp@latest", "--isolated", "--headless=true"]
|
|
133
|
-
},
|
|
134
|
-
"_example_custom_chrome": {
|
|
135
|
-
"mcp_args": ["-y", "chrome-devtools-mcp@latest", "--isolated", "--executablePath=/usr/local/bin/chromium-mcp"]
|
|
136
|
-
},
|
|
137
|
-
"_example_sandbox": {
|
|
138
|
-
"mcp_args": ["-y", "chrome-devtools-mcp@latest", "--isolated=true", "--headless=true", "--executablePath=/usr/local/bin/chromium-mcp"]
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
with open(CONFIG_FILE, "w") as f:
|
|
142
|
-
json.dump(config, f, indent=2)
|
|
143
|
-
print(f"Created config file: {CONFIG_FILE}", file=sys.stderr)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# ============================================================================
|
|
147
|
-
# DAEMON MODE - Bridge between Unix socket and MCP stdio
|
|
148
|
-
# ============================================================================
|
|
149
|
-
|
|
150
|
-
async def read_mcp_responses():
|
|
151
|
-
"""Read responses from MCP stdout and route to clients."""
|
|
152
|
-
global mcp_reader, pending_requests
|
|
153
|
-
|
|
154
|
-
while True:
|
|
155
|
-
try:
|
|
156
|
-
# Read line with increased limit for large snapshots
|
|
157
|
-
line = await mcp_reader.readline()
|
|
158
|
-
if not line:
|
|
159
|
-
print("MCP server closed stdout", file=sys.stderr)
|
|
160
|
-
break
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
resp = json.loads(line.decode())
|
|
164
|
-
msg_id = resp.get("id")
|
|
165
|
-
|
|
166
|
-
# Route response to waiting client
|
|
167
|
-
if msg_id and msg_id in pending_requests:
|
|
168
|
-
future = pending_requests.pop(msg_id)
|
|
169
|
-
future.set_result(resp)
|
|
170
|
-
except json.JSONDecodeError as e:
|
|
171
|
-
print(f"Invalid JSON from MCP: {e}", file=sys.stderr)
|
|
172
|
-
print(f"Line length: {len(line)}", file=sys.stderr)
|
|
173
|
-
|
|
174
|
-
except Exception as e:
|
|
175
|
-
print(f"Error reading MCP: {e}", file=sys.stderr)
|
|
176
|
-
# Don't break on limit errors, just log and continue
|
|
177
|
-
if "longer than limit" not in str(e):
|
|
178
|
-
break
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
|
182
|
-
"""Handle a client connection."""
|
|
183
|
-
global mcp_writer, pending_requests, last_activity_time
|
|
184
|
-
|
|
185
|
-
# Update activity timestamp
|
|
186
|
-
last_activity_time = time.time()
|
|
187
|
-
|
|
188
|
-
try:
|
|
189
|
-
while True:
|
|
190
|
-
# Read request from client
|
|
191
|
-
line = await reader.readline()
|
|
192
|
-
if not line:
|
|
193
|
-
break
|
|
194
|
-
|
|
195
|
-
try:
|
|
196
|
-
req = json.loads(line.decode())
|
|
197
|
-
msg_id = req.get("id")
|
|
198
|
-
|
|
199
|
-
# Create future for response
|
|
200
|
-
response_future = asyncio.Future()
|
|
201
|
-
pending_requests[msg_id] = response_future
|
|
202
|
-
|
|
203
|
-
# Forward to MCP server
|
|
204
|
-
mcp_writer.write(line)
|
|
205
|
-
await mcp_writer.drain()
|
|
206
|
-
|
|
207
|
-
# Wait for response
|
|
208
|
-
try:
|
|
209
|
-
resp = await asyncio.wait_for(response_future, timeout=30.0)
|
|
210
|
-
writer.write(json.dumps(resp).encode() + b'\n')
|
|
211
|
-
await writer.drain()
|
|
212
|
-
except asyncio.TimeoutError:
|
|
213
|
-
writer.write(json.dumps({
|
|
214
|
-
"jsonrpc": "2.0",
|
|
215
|
-
"id": msg_id,
|
|
216
|
-
"error": {"code": -1, "message": "Timeout waiting for MCP response"}
|
|
217
|
-
}).encode() + b'\n')
|
|
218
|
-
await writer.drain()
|
|
219
|
-
pending_requests.pop(msg_id, None)
|
|
220
|
-
|
|
221
|
-
except json.JSONDecodeError:
|
|
222
|
-
print(f"Invalid JSON from client: {line}", file=sys.stderr)
|
|
223
|
-
|
|
224
|
-
except Exception as e:
|
|
225
|
-
print(f"Client handler error: {e}", file=sys.stderr)
|
|
226
|
-
finally:
|
|
227
|
-
writer.close()
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
async def shutdown_mcp():
|
|
231
|
-
"""Properly shutdown MCP server and all Chrome child processes."""
|
|
232
|
-
global mcp_proc
|
|
233
|
-
|
|
234
|
-
if not mcp_proc:
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
print("Shutting down MCP server and Chrome...", file=sys.stderr)
|
|
238
|
-
|
|
239
|
-
try:
|
|
240
|
-
# Get process group ID
|
|
241
|
-
pgid = os.getpgid(mcp_proc.pid)
|
|
242
|
-
|
|
243
|
-
# Send SIGTERM to entire process group (kills Chrome and all children)
|
|
244
|
-
os.killpg(pgid, signal.SIGTERM)
|
|
245
|
-
|
|
246
|
-
# Wait for graceful shutdown
|
|
247
|
-
try:
|
|
248
|
-
await asyncio.wait_for(mcp_proc.wait(), timeout=5.0)
|
|
249
|
-
print("MCP server stopped gracefully", file=sys.stderr)
|
|
250
|
-
except asyncio.TimeoutError:
|
|
251
|
-
# Force kill if still running
|
|
252
|
-
print("Force killing MCP server...", file=sys.stderr)
|
|
253
|
-
os.killpg(pgid, signal.SIGKILL)
|
|
254
|
-
await mcp_proc.wait()
|
|
255
|
-
|
|
256
|
-
except ProcessLookupError:
|
|
257
|
-
# Already dead
|
|
258
|
-
pass
|
|
259
|
-
except Exception as e:
|
|
260
|
-
print(f"Error during shutdown: {e}", file=sys.stderr)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
async def inactivity_monitor(shutdown_callback):
|
|
264
|
-
"""Monitor for inactivity and trigger shutdown after timeout."""
|
|
265
|
-
global last_activity_time
|
|
266
|
-
|
|
267
|
-
while True:
|
|
268
|
-
await asyncio.sleep(10) # Check every 10 seconds for responsive timeout
|
|
269
|
-
inactive_time = time.time() - last_activity_time
|
|
270
|
-
if inactive_time > INACTIVITY_TIMEOUT:
|
|
271
|
-
await shutdown_callback()
|
|
272
|
-
return
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
async def run_daemon(instance_id: str):
|
|
276
|
-
"""Run the persistent daemon."""
|
|
277
|
-
global mcp_proc, mcp_reader, mcp_writer, current_instance_id, last_activity_time
|
|
278
|
-
|
|
279
|
-
current_instance_id = instance_id
|
|
280
|
-
last_activity_time = time.time()
|
|
281
|
-
ensure_state_dir()
|
|
282
|
-
|
|
283
|
-
socket_path = get_socket_path(instance_id)
|
|
284
|
-
pid_file = get_pid_file(instance_id)
|
|
285
|
-
|
|
286
|
-
# Clean up old socket if exists
|
|
287
|
-
if socket_path.exists():
|
|
288
|
-
socket_path.unlink()
|
|
289
|
-
|
|
290
|
-
# Load config
|
|
291
|
-
config = load_config()
|
|
292
|
-
mcp_command = config.get("mcp_command", "npx")
|
|
293
|
-
mcp_args = config.get("mcp_args", ["-y", "chrome-devtools-mcp@latest", "--isolated"])
|
|
294
|
-
|
|
295
|
-
print(f"Starting chrome-devtools-mcp...", file=sys.stderr)
|
|
296
|
-
print(f"Command: {mcp_command} {' '.join(mcp_args)}", file=sys.stderr)
|
|
297
|
-
|
|
298
|
-
# Start MCP server as subprocess in new process group
|
|
299
|
-
# This ensures we can kill Chrome and all its children together
|
|
300
|
-
mcp_proc = await asyncio.create_subprocess_exec(
|
|
301
|
-
mcp_command, *mcp_args,
|
|
302
|
-
stdin=asyncio.subprocess.PIPE,
|
|
303
|
-
stdout=asyncio.subprocess.PIPE,
|
|
304
|
-
stderr=sys.stderr,
|
|
305
|
-
start_new_session=True, # Creates new process group
|
|
306
|
-
limit=1024 * 1024 * 10, # 10MB limit for large snapshots
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
mcp_reader = mcp_proc.stdout
|
|
310
|
-
mcp_writer = mcp_proc.stdin
|
|
311
|
-
|
|
312
|
-
# Write PID file
|
|
313
|
-
pid_file.write_text(str(os.getpid()))
|
|
314
|
-
|
|
315
|
-
print(f"Starting Unix socket server at {socket_path}...", file=sys.stderr)
|
|
316
|
-
|
|
317
|
-
# Start Unix socket server
|
|
318
|
-
server = await asyncio.start_unix_server(
|
|
319
|
-
handle_client,
|
|
320
|
-
path=str(socket_path)
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
print(f"Daemon ready. Instance: {instance_id} PID: {os.getpid()}", file=sys.stderr)
|
|
324
|
-
|
|
325
|
-
# Output instance ID to stdout for capture (all other output goes to stderr)
|
|
326
|
-
print(instance_id)
|
|
327
|
-
|
|
328
|
-
# Setup signal handlers for graceful shutdown
|
|
329
|
-
loop = asyncio.get_event_loop()
|
|
330
|
-
shutdown_triggered = asyncio.Event()
|
|
331
|
-
|
|
332
|
-
async def handle_shutdown(sig=None):
|
|
333
|
-
if shutdown_triggered.is_set():
|
|
334
|
-
return
|
|
335
|
-
shutdown_triggered.set()
|
|
336
|
-
if sig:
|
|
337
|
-
print(f"\nReceived {sig.name}, shutting down...", file=sys.stderr)
|
|
338
|
-
await shutdown_mcp()
|
|
339
|
-
server.close()
|
|
340
|
-
await server.wait_closed()
|
|
341
|
-
socket_path.unlink(missing_ok=True)
|
|
342
|
-
pid_file.unlink(missing_ok=True)
|
|
343
|
-
# Exit cleanly instead of stopping loop (avoids RuntimeError)
|
|
344
|
-
os._exit(0)
|
|
345
|
-
|
|
346
|
-
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
347
|
-
loop.add_signal_handler(
|
|
348
|
-
sig,
|
|
349
|
-
lambda s=sig: asyncio.create_task(handle_shutdown(s))
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
# Run socket server, MCP response reader, and inactivity monitor
|
|
353
|
-
try:
|
|
354
|
-
await asyncio.gather(
|
|
355
|
-
server.serve_forever(),
|
|
356
|
-
read_mcp_responses(),
|
|
357
|
-
inactivity_monitor(handle_shutdown),
|
|
358
|
-
)
|
|
359
|
-
except KeyboardInterrupt:
|
|
360
|
-
print("\nShutting down daemon...", file=sys.stderr)
|
|
361
|
-
finally:
|
|
362
|
-
# Cleanup
|
|
363
|
-
if not shutdown_triggered.is_set():
|
|
364
|
-
await shutdown_mcp()
|
|
365
|
-
server.close()
|
|
366
|
-
await server.wait_closed()
|
|
367
|
-
socket_path.unlink(missing_ok=True)
|
|
368
|
-
pid_file.unlink(missing_ok=True)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
# ============================================================================
|
|
372
|
-
# CLIENT MODE - Connect to daemon and send commands
|
|
373
|
-
# ============================================================================
|
|
374
|
-
|
|
375
|
-
async def send_command(instance_id: str, tool_name: str, args: Dict[str, Any]) -> Any:
|
|
376
|
-
"""Send a command to the daemon via socket."""
|
|
377
|
-
if not is_daemon_running(instance_id):
|
|
378
|
-
raise RuntimeError(f"Daemon instance '{instance_id}' not running. Start with: browsertools.py daemon start")
|
|
379
|
-
|
|
380
|
-
socket_path = get_socket_path(instance_id)
|
|
381
|
-
|
|
382
|
-
# Connect to daemon's Unix socket with increased limit for large responses
|
|
383
|
-
reader, writer = await asyncio.open_unix_connection(
|
|
384
|
-
str(socket_path),
|
|
385
|
-
limit=1024 * 1024 * 10 # 10MB limit for large snapshots
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
try:
|
|
389
|
-
# Create JSON-RPC request
|
|
390
|
-
msg_id = os.urandom(8).hex()
|
|
391
|
-
request = {
|
|
392
|
-
"jsonrpc": "2.0",
|
|
393
|
-
"method": "tools/call",
|
|
394
|
-
"params": {
|
|
395
|
-
"name": tool_name,
|
|
396
|
-
"arguments": args
|
|
397
|
-
},
|
|
398
|
-
"id": msg_id
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
# Send request
|
|
402
|
-
writer.write(json.dumps(request).encode() + b'\n')
|
|
403
|
-
await writer.drain()
|
|
404
|
-
|
|
405
|
-
# Read response
|
|
406
|
-
line = await reader.readline()
|
|
407
|
-
response = json.loads(line.decode())
|
|
408
|
-
|
|
409
|
-
if "error" in response:
|
|
410
|
-
raise RuntimeError(f"MCP error: {response['error']}")
|
|
411
|
-
|
|
412
|
-
return response.get("result")
|
|
413
|
-
|
|
414
|
-
finally:
|
|
415
|
-
writer.close()
|
|
416
|
-
await writer.wait_closed()
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def clean_output(result: Any) -> str:
|
|
420
|
-
"""Extract clean text from MCP result."""
|
|
421
|
-
if isinstance(result, dict):
|
|
422
|
-
if "content" in result:
|
|
423
|
-
content = result["content"]
|
|
424
|
-
if isinstance(content, list):
|
|
425
|
-
texts = []
|
|
426
|
-
for item in content:
|
|
427
|
-
if isinstance(item, dict):
|
|
428
|
-
if "text" in item:
|
|
429
|
-
texts.append(item["text"])
|
|
430
|
-
elif item.get("type") == "image" and "data" in item:
|
|
431
|
-
# Return base64 image data
|
|
432
|
-
mime = item.get("mimeType", "image/png")
|
|
433
|
-
b64_data = item["data"]
|
|
434
|
-
texts.append(f"\n## Base64 Image ({mime})")
|
|
435
|
-
texts.append(b64_data)
|
|
436
|
-
|
|
437
|
-
return "\n".join(texts) if texts else str(content)
|
|
438
|
-
elif "text" in result:
|
|
439
|
-
return result["text"]
|
|
440
|
-
return str(result)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
async def execute_command(instance_id: str, cmd: str, cmd_args: Dict[str, Any]):
|
|
444
|
-
"""Execute a single command via daemon."""
|
|
445
|
-
|
|
446
|
-
# Map commands to MCP tools
|
|
447
|
-
tool_name = None
|
|
448
|
-
tool_args = {}
|
|
449
|
-
|
|
450
|
-
if cmd == "nav":
|
|
451
|
-
tool_name = "navigate_page"
|
|
452
|
-
tool_args = {"url": cmd_args["url"]}
|
|
453
|
-
if "timeout" in cmd_args:
|
|
454
|
-
tool_args["timeout"] = cmd_args["timeout"]
|
|
455
|
-
elif cmd == "snap":
|
|
456
|
-
tool_name = "take_snapshot"
|
|
457
|
-
elif cmd == "click":
|
|
458
|
-
tool_name = "click"
|
|
459
|
-
tool_args = {"uid": cmd_args["uid"]}
|
|
460
|
-
elif cmd == "fill":
|
|
461
|
-
tool_name = "fill"
|
|
462
|
-
tool_args = {"uid": cmd_args["uid"], "value": cmd_args["value"]}
|
|
463
|
-
elif cmd == "shot":
|
|
464
|
-
tool_name = "take_screenshot"
|
|
465
|
-
if "path" in cmd_args:
|
|
466
|
-
tool_args["filePath"] = cmd_args["path"]
|
|
467
|
-
elif cmd == "wait":
|
|
468
|
-
tool_name = "wait_for"
|
|
469
|
-
tool_args = {"text": cmd_args["text"]}
|
|
470
|
-
if "timeout" in cmd_args:
|
|
471
|
-
tool_args["timeout"] = cmd_args["timeout"]
|
|
472
|
-
elif cmd == "eval":
|
|
473
|
-
tool_name = "evaluate_script"
|
|
474
|
-
tool_args = {"function": cmd_args["function"]}
|
|
475
|
-
if "args" in cmd_args:
|
|
476
|
-
tool_args["args"] = cmd_args["args"]
|
|
477
|
-
elif cmd == "key":
|
|
478
|
-
tool_name = "press_key"
|
|
479
|
-
tool_args = {"key": cmd_args["key"]}
|
|
480
|
-
elif cmd == "hover":
|
|
481
|
-
tool_name = "hover"
|
|
482
|
-
tool_args = {"uid": cmd_args["uid"]}
|
|
483
|
-
elif cmd == "netlist":
|
|
484
|
-
tool_name = "list_network_requests"
|
|
485
|
-
if "resource_types" in cmd_args:
|
|
486
|
-
tool_args["resourceTypes"] = cmd_args["resource_types"]
|
|
487
|
-
if "page_size" in cmd_args:
|
|
488
|
-
tool_args["pageSize"] = cmd_args["page_size"]
|
|
489
|
-
elif cmd == "netget":
|
|
490
|
-
tool_name = "get_network_request"
|
|
491
|
-
if "reqid" in cmd_args:
|
|
492
|
-
tool_args["reqid"] = cmd_args["reqid"]
|
|
493
|
-
elif cmd == "conslist":
|
|
494
|
-
tool_name = "list_console_messages"
|
|
495
|
-
if "types" in cmd_args:
|
|
496
|
-
tool_args["types"] = cmd_args["types"]
|
|
497
|
-
if "page_size" in cmd_args:
|
|
498
|
-
tool_args["pageSize"] = cmd_args["page_size"]
|
|
499
|
-
elif cmd == "consget":
|
|
500
|
-
tool_name = "get_console_message"
|
|
501
|
-
tool_args = {"msgid": cmd_args["msgid"]}
|
|
502
|
-
elif cmd == "resize":
|
|
503
|
-
tool_name = "resize_page"
|
|
504
|
-
tool_args = {"width": cmd_args["width"], "height": cmd_args["height"]}
|
|
505
|
-
elif cmd == "dialog":
|
|
506
|
-
tool_name = "handle_dialog"
|
|
507
|
-
tool_args = {"action": cmd_args["action"]}
|
|
508
|
-
if "prompt_text" in cmd_args:
|
|
509
|
-
tool_args["promptText"] = cmd_args["prompt_text"]
|
|
510
|
-
elif cmd == "upload":
|
|
511
|
-
tool_name = "upload_file"
|
|
512
|
-
tool_args = {"uid": cmd_args["uid"], "filePath": cmd_args["file_path"]}
|
|
513
|
-
elif cmd == "drag":
|
|
514
|
-
tool_name = "drag"
|
|
515
|
-
tool_args = {"from_uid": cmd_args["from_uid"], "to_uid": cmd_args["to_uid"]}
|
|
516
|
-
elif cmd == "fillform":
|
|
517
|
-
tool_name = "fill_form"
|
|
518
|
-
tool_args = {"elements": cmd_args["elements"]}
|
|
519
|
-
else:
|
|
520
|
-
raise ValueError(f"Unknown command: {cmd}")
|
|
521
|
-
|
|
522
|
-
# Send to daemon
|
|
523
|
-
result = await send_command(instance_id, tool_name, tool_args)
|
|
524
|
-
return clean_output(result)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
# ============================================================================
|
|
528
|
-
# CLI INTERFACE
|
|
529
|
-
# ============================================================================
|
|
530
|
-
|
|
531
|
-
async def main():
|
|
532
|
-
parser = argparse.ArgumentParser(
|
|
533
|
-
prog="browsertools.py",
|
|
534
|
-
description="Chrome DevTools MCP wrapper - multi-instance daemon mode",
|
|
535
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
536
|
-
epilog=r"""
|
|
537
|
-
browsertools.py - Chrome DevTools automation tool
|
|
538
|
-
|
|
539
|
-
Usage: uv run .agents/tools/chrome-devtools/browsertools.py <command>
|
|
540
|
-
|
|
541
|
-
DAEMON MANAGEMENT:
|
|
542
|
-
daemon start Start new daemon instance
|
|
543
|
-
Returns: "browsertools daemon started. Instance ID: <id>"
|
|
544
|
-
daemon stop <id> Stop daemon. kill chrome cleanly.
|
|
545
|
-
daemon config Create config file
|
|
546
|
-
|
|
547
|
-
ALL OTHER COMMANDS REQUIRE --instance <id>:
|
|
548
|
-
--instance <id> Required for all browser commands
|
|
549
|
-
|
|
550
|
-
NAVIGATION:
|
|
551
|
-
nav <url> Navigate to URL
|
|
552
|
-
--timeout <ms> Max wait time in milliseconds
|
|
553
|
-
wait <text> Wait for text to appear
|
|
554
|
-
--timeout <ms> Max wait time in milliseconds
|
|
555
|
-
|
|
556
|
-
INSPECTION:
|
|
557
|
-
snap Take a11y tree snapshot (get element UIDs)
|
|
558
|
-
IMPORTANT: Always use latest snapshot for UIDs
|
|
559
|
-
shot [path] Screenshot viewport (optional: save to path)
|
|
560
|
-
eval <js> Execute JavaScript, returns JSON
|
|
561
|
-
--args <json> JSON array of element references (advanced)
|
|
562
|
-
|
|
563
|
-
INTERACTION:
|
|
564
|
-
click <uid> Click element by UID from snapshot
|
|
565
|
-
fill <uid> <value> Fill input/textarea or select option
|
|
566
|
-
key <key> Press key or combo (e.g., "Enter", "Tab", "Control+A")
|
|
567
|
-
hover <uid> Hover over element
|
|
568
|
-
drag <from> <to> Drag element from UID to target UID
|
|
569
|
-
fillform <json> Fill multiple form fields
|
|
570
|
-
|
|
571
|
-
DEBUGGING:
|
|
572
|
-
conslist List console messages (--types, --size)
|
|
573
|
-
consget <msgid> Get console message details
|
|
574
|
-
netlist List network requests (--types, --size)
|
|
575
|
-
netget [reqid] Get request details
|
|
576
|
-
|
|
577
|
-
PAGE MANIPULATION:
|
|
578
|
-
resize <w> <h> Resize viewport
|
|
579
|
-
dialog <action> Handle alert/confirm/prompt (accept|dismiss)
|
|
580
|
-
upload <uid> <path> Upload file
|
|
581
|
-
|
|
582
|
-
USAGE TIPS:
|
|
583
|
-
**Snapshots** are verbose. Always try to pipe grep first
|
|
584
|
-
- Default pattern: snap | grep -iC5 "search term"
|
|
585
|
-
- **Interaction commands** return updated page state snapshots: click, fill, key, hover, drag, wait return updated page state
|
|
586
|
-
- Pipe to grep with context. eg. click 1_7 | grep -iC5 "dashboard\|home"
|
|
587
|
-
- No grep hits? Run explicit snap to see full page
|
|
588
|
-
|
|
589
|
-
Limit output for **debugging** commands
|
|
590
|
-
- Network: netlist --size 10 --types fetch
|
|
591
|
-
- Console: conslist --size 5 --types error
|
|
592
|
-
|
|
593
|
-
EXAMPLES:
|
|
594
|
-
# Start daemon (backgrounds automatically, prints instance ID)
|
|
595
|
-
uv run .agents/tools/chrome-devtools/browsertools.py daemon start
|
|
596
|
-
# Output: browsertools daemon started. Instance ID: a3f7b2c1
|
|
597
|
-
|
|
598
|
-
# Use instance for all commands
|
|
599
|
-
uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 nav http://app.com/login
|
|
600
|
-
uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 snap | grep -iC5 "email\|username\|login"
|
|
601
|
-
uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 fill 1_23 user@example.com
|
|
602
|
-
uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 snap | grep -iC5 "button.*(submit|login|sign.?in)"
|
|
603
|
-
uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 click 1_25 | grep -iC5 "dashboard\|home\|welcome"
|
|
604
|
-
|
|
605
|
-
# Stop when finished to clean up resources
|
|
606
|
-
uv run .agents/tools/chrome-devtools/browsertools.py daemon stop a3f7b2c1
|
|
607
|
-
"""
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
# Global --instance argument
|
|
611
|
-
parser.add_argument("--instance", "-i", help="Daemon instance ID (required for browser commands)")
|
|
612
|
-
|
|
613
|
-
subparsers = parser.add_subparsers(dest="cmd", required=True)
|
|
614
|
-
|
|
615
|
-
# Daemon management
|
|
616
|
-
daemon = subparsers.add_parser("daemon", help="Manage daemon")
|
|
617
|
-
daemon_sub = daemon.add_subparsers(dest="daemon_cmd", required=True)
|
|
618
|
-
daemon_sub.add_parser("start", help="Start new daemon instance (returns instance ID)")
|
|
619
|
-
stop_parser = daemon_sub.add_parser("stop", help="Stop daemon instance")
|
|
620
|
-
stop_parser.add_argument("instance_id", nargs="?", help="Instance ID to stop")
|
|
621
|
-
stop_parser.add_argument("--all", dest="stop_all", action="store_true", help=argparse.SUPPRESS) # Hidden
|
|
622
|
-
daemon_sub.add_parser("config", help="Create default config file")
|
|
623
|
-
|
|
624
|
-
# Browser commands
|
|
625
|
-
nav = subparsers.add_parser("nav", help="Navigate to URL")
|
|
626
|
-
nav.add_argument("url", help="URL to navigate to")
|
|
627
|
-
nav.add_argument("--timeout", type=int, help="Max wait time in milliseconds")
|
|
628
|
-
|
|
629
|
-
subparsers.add_parser("snap", help="Take page snapshot")
|
|
630
|
-
|
|
631
|
-
shot = subparsers.add_parser("shot", help="Take screenshot")
|
|
632
|
-
shot.add_argument("path", nargs="?", help="Save path (optional)")
|
|
633
|
-
|
|
634
|
-
click = subparsers.add_parser("click", help="Click element")
|
|
635
|
-
click.add_argument("uid", help="Element UID from snapshot")
|
|
636
|
-
|
|
637
|
-
fill = subparsers.add_parser("fill", help="Fill input")
|
|
638
|
-
fill.add_argument("uid", help="Element UID")
|
|
639
|
-
fill.add_argument("value", help="Value to fill")
|
|
640
|
-
|
|
641
|
-
wait = subparsers.add_parser("wait", help="Wait for text")
|
|
642
|
-
wait.add_argument("text", help="Text to wait for")
|
|
643
|
-
wait.add_argument("--timeout", type=int, help="Max wait time in milliseconds")
|
|
644
|
-
|
|
645
|
-
eval_parser = subparsers.add_parser("eval", help="Execute JavaScript")
|
|
646
|
-
eval_parser.add_argument("function", help="JS function to execute")
|
|
647
|
-
eval_parser.add_argument("--args", help="JSON array of element UIDs to pass as arguments")
|
|
648
|
-
|
|
649
|
-
# Keyboard & mouse
|
|
650
|
-
key = subparsers.add_parser("key", help="Press keyboard key")
|
|
651
|
-
key.add_argument("key", help="Key to press (e.g., 'Enter', 'Tab', 'Escape')")
|
|
652
|
-
|
|
653
|
-
hover_parser = subparsers.add_parser("hover", help="Hover over element")
|
|
654
|
-
hover_parser.add_argument("uid", help="Element UID from snapshot")
|
|
655
|
-
|
|
656
|
-
# Network debugging
|
|
657
|
-
netlist = subparsers.add_parser("netlist", help="List network requests")
|
|
658
|
-
netlist.add_argument("--types", nargs="+", help="Filter by resource types")
|
|
659
|
-
netlist.add_argument("--size", type=int, help="Max requests to return")
|
|
660
|
-
|
|
661
|
-
netget = subparsers.add_parser("netget", help="Get network request details")
|
|
662
|
-
netget.add_argument("reqid", nargs="?", type=int, help="Request ID (omit for latest)")
|
|
663
|
-
|
|
664
|
-
# Console debugging
|
|
665
|
-
conslist = subparsers.add_parser("conslist", help="List console messages")
|
|
666
|
-
conslist.add_argument("--types", nargs="+", help="Filter by types (log, error, warn)")
|
|
667
|
-
conslist.add_argument("--size", type=int, help="Max messages to return")
|
|
668
|
-
|
|
669
|
-
consget = subparsers.add_parser("consget", help="Get console message details")
|
|
670
|
-
consget.add_argument("msgid", type=int, help="Message ID")
|
|
671
|
-
|
|
672
|
-
# Page manipulation
|
|
673
|
-
resize = subparsers.add_parser("resize", help="Resize page viewport")
|
|
674
|
-
resize.add_argument("width", type=int, help="Width in pixels")
|
|
675
|
-
resize.add_argument("height", type=int, help="Height in pixels")
|
|
676
|
-
|
|
677
|
-
dialog = subparsers.add_parser("dialog", help="Handle browser dialog (alert/confirm/prompt)")
|
|
678
|
-
dialog.add_argument("action", choices=["accept", "dismiss"], help="Accept or dismiss dialog")
|
|
679
|
-
dialog.add_argument("--text", dest="prompt_text", help="Text for prompt dialog")
|
|
680
|
-
|
|
681
|
-
upload = subparsers.add_parser("upload", help="Upload file through element")
|
|
682
|
-
upload.add_argument("uid", help="File input element UID")
|
|
683
|
-
upload.add_argument("file_path", help="Local file path to upload")
|
|
684
|
-
|
|
685
|
-
# Advanced interaction
|
|
686
|
-
drag_parser = subparsers.add_parser("drag", help="Drag element to target")
|
|
687
|
-
drag_parser.add_argument("from_uid", help="Source element UID")
|
|
688
|
-
drag_parser.add_argument("to_uid", help="Target element UID")
|
|
689
|
-
|
|
690
|
-
fillform_parser = subparsers.add_parser("fillform", help="Fill multiple form fields")
|
|
691
|
-
fillform_parser.add_argument("elements", help="JSON array: [{'uid':'1_1','value':'text'}]")
|
|
692
|
-
|
|
693
|
-
args = parser.parse_args()
|
|
694
|
-
|
|
695
|
-
# Handle daemon commands
|
|
696
|
-
if args.cmd == "daemon":
|
|
697
|
-
if args.daemon_cmd == "start":
|
|
698
|
-
# Generate new instance ID
|
|
699
|
-
instance_id = generate_instance_id()
|
|
700
|
-
|
|
701
|
-
# Fork to background
|
|
702
|
-
pid = os.fork()
|
|
703
|
-
|
|
704
|
-
if pid > 0:
|
|
705
|
-
# Parent process - print result and exit
|
|
706
|
-
# Give child a moment to start
|
|
707
|
-
time.sleep(0.5)
|
|
708
|
-
print(f"browsertools daemon started. Instance ID: {instance_id}")
|
|
709
|
-
return # Exit from async main, let asyncio.run() clean up
|
|
710
|
-
else:
|
|
711
|
-
# Child process - detach and run daemon
|
|
712
|
-
os.setsid() # Create new session, detach from terminal
|
|
713
|
-
|
|
714
|
-
# Redirect stdin/stdout/stderr to /dev/null (daemon runs silently)
|
|
715
|
-
devnull_r = os.open(os.devnull, os.O_RDONLY)
|
|
716
|
-
devnull_w = os.open(os.devnull, os.O_WRONLY)
|
|
717
|
-
os.dup2(devnull_r, sys.stdin.fileno())
|
|
718
|
-
os.dup2(devnull_w, sys.stdout.fileno())
|
|
719
|
-
os.dup2(devnull_w, sys.stderr.fileno())
|
|
720
|
-
os.close(devnull_r)
|
|
721
|
-
os.close(devnull_w)
|
|
722
|
-
|
|
723
|
-
# Create fresh event loop for child (parent's loop is unusable after fork)
|
|
724
|
-
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
725
|
-
|
|
726
|
-
# Run the daemon in the new event loop (this blocks until shutdown)
|
|
727
|
-
asyncio.run(run_daemon(instance_id))
|
|
728
|
-
os._exit(0)
|
|
729
|
-
|
|
730
|
-
elif args.daemon_cmd == "stop":
|
|
731
|
-
if args.stop_all:
|
|
732
|
-
# Hidden --all flag: stop all running instances
|
|
733
|
-
instances = get_all_instances()
|
|
734
|
-
if not instances:
|
|
735
|
-
print("No running instances", file=sys.stderr)
|
|
736
|
-
sys.exit(1)
|
|
737
|
-
for inst_id in instances:
|
|
738
|
-
pid = get_daemon_pid(inst_id)
|
|
739
|
-
if pid:
|
|
740
|
-
os.kill(pid, signal.SIGTERM)
|
|
741
|
-
print(f"Stopped instance {inst_id} (PID {pid})", file=sys.stderr)
|
|
742
|
-
elif args.instance_id:
|
|
743
|
-
# Stop specific instance
|
|
744
|
-
pid = get_daemon_pid(args.instance_id)
|
|
745
|
-
if not pid:
|
|
746
|
-
print(f"Instance '{args.instance_id}' not running", file=sys.stderr)
|
|
747
|
-
sys.exit(1)
|
|
748
|
-
os.kill(pid, signal.SIGTERM)
|
|
749
|
-
print(f"Stopped instance {args.instance_id} (PID {pid})", file=sys.stderr)
|
|
750
|
-
else:
|
|
751
|
-
print("Error: instance_id required. Usage: daemon stop <instance_id>", file=sys.stderr)
|
|
752
|
-
sys.exit(1)
|
|
753
|
-
|
|
754
|
-
elif args.daemon_cmd == "config":
|
|
755
|
-
save_default_config()
|
|
756
|
-
print(f"\nEdit {CONFIG_FILE} to customize MCP server settings.", file=sys.stderr)
|
|
757
|
-
print("\nExamples:", file=sys.stderr)
|
|
758
|
-
print(" Headless mode: --headless=true", file=sys.stderr)
|
|
759
|
-
print(" Custom Chrome: --executablePath=/path/to/chrome", file=sys.stderr)
|
|
760
|
-
|
|
761
|
-
return
|
|
762
|
-
|
|
763
|
-
# Execute browser command - requires --instance
|
|
764
|
-
if not args.instance:
|
|
765
|
-
print("Error: --instance <id> is required for browser commands", file=sys.stderr)
|
|
766
|
-
print("Start a daemon first: uv run browsertools.py daemon start", file=sys.stderr)
|
|
767
|
-
sys.exit(1)
|
|
768
|
-
|
|
769
|
-
if not is_daemon_running(args.instance):
|
|
770
|
-
print(f"Error: Instance '{args.instance}' is not running", file=sys.stderr)
|
|
771
|
-
sys.exit(1)
|
|
772
|
-
|
|
773
|
-
try:
|
|
774
|
-
cmd_args_dict = {}
|
|
775
|
-
|
|
776
|
-
if args.cmd == "nav":
|
|
777
|
-
cmd_args_dict["url"] = args.url
|
|
778
|
-
if args.timeout:
|
|
779
|
-
cmd_args_dict["timeout"] = args.timeout
|
|
780
|
-
elif args.cmd == "click":
|
|
781
|
-
cmd_args_dict["uid"] = args.uid
|
|
782
|
-
elif args.cmd == "fill":
|
|
783
|
-
cmd_args_dict["uid"] = args.uid
|
|
784
|
-
cmd_args_dict["value"] = args.value
|
|
785
|
-
elif args.cmd == "shot" and args.path:
|
|
786
|
-
cmd_args_dict["path"] = args.path
|
|
787
|
-
elif args.cmd == "wait":
|
|
788
|
-
cmd_args_dict["text"] = args.text
|
|
789
|
-
if args.timeout:
|
|
790
|
-
cmd_args_dict["timeout"] = args.timeout
|
|
791
|
-
elif args.cmd == "eval":
|
|
792
|
-
cmd_args_dict["function"] = args.function
|
|
793
|
-
if args.args:
|
|
794
|
-
cmd_args_dict["args"] = json.loads(args.args)
|
|
795
|
-
elif args.cmd == "key":
|
|
796
|
-
cmd_args_dict["key"] = args.key
|
|
797
|
-
elif args.cmd == "hover":
|
|
798
|
-
cmd_args_dict["uid"] = args.uid
|
|
799
|
-
elif args.cmd == "netlist":
|
|
800
|
-
if args.types:
|
|
801
|
-
cmd_args_dict["resource_types"] = args.types
|
|
802
|
-
if args.size:
|
|
803
|
-
cmd_args_dict["page_size"] = args.size
|
|
804
|
-
elif args.cmd == "netget":
|
|
805
|
-
if args.reqid:
|
|
806
|
-
cmd_args_dict["reqid"] = args.reqid
|
|
807
|
-
elif args.cmd == "conslist":
|
|
808
|
-
if args.types:
|
|
809
|
-
cmd_args_dict["types"] = args.types
|
|
810
|
-
if args.size:
|
|
811
|
-
cmd_args_dict["page_size"] = args.size
|
|
812
|
-
elif args.cmd == "consget":
|
|
813
|
-
cmd_args_dict["msgid"] = args.msgid
|
|
814
|
-
elif args.cmd == "resize":
|
|
815
|
-
cmd_args_dict["width"] = args.width
|
|
816
|
-
cmd_args_dict["height"] = args.height
|
|
817
|
-
elif args.cmd == "dialog":
|
|
818
|
-
cmd_args_dict["action"] = args.action
|
|
819
|
-
if args.prompt_text:
|
|
820
|
-
cmd_args_dict["prompt_text"] = args.prompt_text
|
|
821
|
-
elif args.cmd == "upload":
|
|
822
|
-
cmd_args_dict["uid"] = args.uid
|
|
823
|
-
cmd_args_dict["file_path"] = args.file_path
|
|
824
|
-
elif args.cmd == "drag":
|
|
825
|
-
cmd_args_dict["from_uid"] = args.from_uid
|
|
826
|
-
cmd_args_dict["to_uid"] = args.to_uid
|
|
827
|
-
elif args.cmd == "fillform":
|
|
828
|
-
cmd_args_dict["elements"] = json.loads(args.elements)
|
|
829
|
-
|
|
830
|
-
output = await execute_command(args.instance, args.cmd, cmd_args_dict)
|
|
831
|
-
print(output)
|
|
832
|
-
|
|
833
|
-
except Exception as e:
|
|
834
|
-
print(f"Error: {e}", file=sys.stderr)
|
|
835
|
-
sys.exit(1)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if __name__ == "__main__":
|
|
839
|
-
asyncio.run(main())
|