claude-comms 1.0.39 → 1.0.40
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/.agents/tools/chrome-devtools/browsertools.py +207 -104
- package/.claude/commands/await-feedback.md +1 -0
- package/.claude/commands/cook.md +4 -4
- package/.claude/commands/uat-refine.md +1 -1
- package/CLAUDE.md +4 -4
- package/package.json +1 -1
- package/src/orchestrator/installer.js +44 -24
- package/.claude/commands/new-agents.md +0 -11
|
@@ -21,20 +21,38 @@ import json
|
|
|
21
21
|
import os
|
|
22
22
|
import signal
|
|
23
23
|
import sys
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
24
26
|
from pathlib import Path
|
|
25
27
|
from typing import Any, Dict, Optional
|
|
26
28
|
|
|
27
29
|
# State directory
|
|
28
30
|
STATE_DIR = Path.home() / ".browsertools"
|
|
29
|
-
SOCKET_PATH = STATE_DIR / "daemon.sock"
|
|
30
|
-
PID_FILE = STATE_DIR / "daemon.pid"
|
|
31
31
|
CONFIG_FILE = STATE_DIR / "config.json"
|
|
32
|
+
INACTIVITY_TIMEOUT = 600 # 10 minutes
|
|
32
33
|
|
|
33
34
|
# Global daemon state
|
|
34
35
|
mcp_proc = None
|
|
35
36
|
mcp_reader = None
|
|
36
37
|
mcp_writer = None
|
|
37
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"
|
|
38
56
|
|
|
39
57
|
|
|
40
58
|
def ensure_state_dir():
|
|
@@ -42,22 +60,39 @@ def ensure_state_dir():
|
|
|
42
60
|
STATE_DIR.mkdir(exist_ok=True)
|
|
43
61
|
|
|
44
62
|
|
|
45
|
-
def get_daemon_pid() -> Optional[int]:
|
|
46
|
-
"""Get PID of running daemon."""
|
|
47
|
-
|
|
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():
|
|
48
67
|
return None
|
|
49
68
|
try:
|
|
50
|
-
pid = int(
|
|
69
|
+
pid = int(pid_file.read_text().strip())
|
|
51
70
|
os.kill(pid, 0) # Check if alive
|
|
52
71
|
return pid
|
|
53
72
|
except (ProcessLookupError, ValueError):
|
|
54
|
-
|
|
73
|
+
pid_file.unlink(missing_ok=True)
|
|
55
74
|
return None
|
|
56
75
|
|
|
57
76
|
|
|
58
|
-
def is_daemon_running() -> bool:
|
|
59
|
-
"""Check if daemon is running."""
|
|
60
|
-
return get_daemon_pid() is not None and
|
|
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
|
|
61
96
|
|
|
62
97
|
|
|
63
98
|
def load_config() -> Dict[str, Any]:
|
|
@@ -145,7 +180,10 @@ async def read_mcp_responses():
|
|
|
145
180
|
|
|
146
181
|
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
|
147
182
|
"""Handle a client connection."""
|
|
148
|
-
global mcp_writer, pending_requests
|
|
183
|
+
global mcp_writer, pending_requests, last_activity_time
|
|
184
|
+
|
|
185
|
+
# Update activity timestamp
|
|
186
|
+
last_activity_time = time.time()
|
|
149
187
|
|
|
150
188
|
try:
|
|
151
189
|
while True:
|
|
@@ -222,15 +260,32 @@ async def shutdown_mcp():
|
|
|
222
260
|
print(f"Error during shutdown: {e}", file=sys.stderr)
|
|
223
261
|
|
|
224
262
|
|
|
225
|
-
async def
|
|
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):
|
|
226
276
|
"""Run the persistent daemon."""
|
|
227
|
-
global mcp_proc, mcp_reader, mcp_writer
|
|
277
|
+
global mcp_proc, mcp_reader, mcp_writer, current_instance_id, last_activity_time
|
|
228
278
|
|
|
279
|
+
current_instance_id = instance_id
|
|
280
|
+
last_activity_time = time.time()
|
|
229
281
|
ensure_state_dir()
|
|
230
282
|
|
|
283
|
+
socket_path = get_socket_path(instance_id)
|
|
284
|
+
pid_file = get_pid_file(instance_id)
|
|
285
|
+
|
|
231
286
|
# Clean up old socket if exists
|
|
232
|
-
if
|
|
233
|
-
|
|
287
|
+
if socket_path.exists():
|
|
288
|
+
socket_path.unlink()
|
|
234
289
|
|
|
235
290
|
# Load config
|
|
236
291
|
config = load_config()
|
|
@@ -255,29 +310,38 @@ async def run_daemon():
|
|
|
255
310
|
mcp_writer = mcp_proc.stdin
|
|
256
311
|
|
|
257
312
|
# Write PID file
|
|
258
|
-
|
|
313
|
+
pid_file.write_text(str(os.getpid()))
|
|
259
314
|
|
|
260
|
-
print(f"Starting Unix socket server at {
|
|
315
|
+
print(f"Starting Unix socket server at {socket_path}...", file=sys.stderr)
|
|
261
316
|
|
|
262
317
|
# Start Unix socket server
|
|
263
318
|
server = await asyncio.start_unix_server(
|
|
264
319
|
handle_client,
|
|
265
|
-
path=str(
|
|
320
|
+
path=str(socket_path)
|
|
266
321
|
)
|
|
267
322
|
|
|
268
|
-
print(f"Daemon ready. PID: {os.getpid()}", file=sys.stderr)
|
|
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)
|
|
269
327
|
|
|
270
328
|
# Setup signal handlers for graceful shutdown
|
|
271
329
|
loop = asyncio.get_event_loop()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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)
|
|
275
338
|
await shutdown_mcp()
|
|
276
339
|
server.close()
|
|
277
340
|
await server.wait_closed()
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
loop
|
|
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)
|
|
281
345
|
|
|
282
346
|
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
283
347
|
loop.add_signal_handler(
|
|
@@ -285,35 +349,39 @@ async def run_daemon():
|
|
|
285
349
|
lambda s=sig: asyncio.create_task(handle_shutdown(s))
|
|
286
350
|
)
|
|
287
351
|
|
|
288
|
-
# Run
|
|
352
|
+
# Run socket server, MCP response reader, and inactivity monitor
|
|
289
353
|
try:
|
|
290
354
|
await asyncio.gather(
|
|
291
355
|
server.serve_forever(),
|
|
292
356
|
read_mcp_responses(),
|
|
357
|
+
inactivity_monitor(handle_shutdown),
|
|
293
358
|
)
|
|
294
359
|
except KeyboardInterrupt:
|
|
295
360
|
print("\nShutting down daemon...", file=sys.stderr)
|
|
296
361
|
finally:
|
|
297
362
|
# Cleanup
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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)
|
|
303
369
|
|
|
304
370
|
|
|
305
371
|
# ============================================================================
|
|
306
372
|
# CLIENT MODE - Connect to daemon and send commands
|
|
307
373
|
# ============================================================================
|
|
308
374
|
|
|
309
|
-
async def send_command(tool_name: str, args: Dict[str, Any]) -> Any:
|
|
375
|
+
async def send_command(instance_id: str, tool_name: str, args: Dict[str, Any]) -> Any:
|
|
310
376
|
"""Send a command to the daemon via socket."""
|
|
311
|
-
if not is_daemon_running():
|
|
312
|
-
raise RuntimeError("Daemon not running. Start with:
|
|
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)
|
|
313
381
|
|
|
314
382
|
# Connect to daemon's Unix socket with increased limit for large responses
|
|
315
383
|
reader, writer = await asyncio.open_unix_connection(
|
|
316
|
-
str(
|
|
384
|
+
str(socket_path),
|
|
317
385
|
limit=1024 * 1024 * 10 # 10MB limit for large snapshots
|
|
318
386
|
)
|
|
319
387
|
|
|
@@ -372,7 +440,7 @@ def clean_output(result: Any) -> str:
|
|
|
372
440
|
return str(result)
|
|
373
441
|
|
|
374
442
|
|
|
375
|
-
async def execute_command(cmd: str, cmd_args: Dict[str, Any]):
|
|
443
|
+
async def execute_command(instance_id: str, cmd: str, cmd_args: Dict[str, Any]):
|
|
376
444
|
"""Execute a single command via daemon."""
|
|
377
445
|
|
|
378
446
|
# Map commands to MCP tools
|
|
@@ -452,7 +520,7 @@ async def execute_command(cmd: str, cmd_args: Dict[str, Any]):
|
|
|
452
520
|
raise ValueError(f"Unknown command: {cmd}")
|
|
453
521
|
|
|
454
522
|
# Send to daemon
|
|
455
|
-
result = await send_command(tool_name, tool_args)
|
|
523
|
+
result = await send_command(instance_id, tool_name, tool_args)
|
|
456
524
|
return clean_output(result)
|
|
457
525
|
|
|
458
526
|
|
|
@@ -463,22 +531,22 @@ async def execute_command(cmd: str, cmd_args: Dict[str, Any]):
|
|
|
463
531
|
async def main():
|
|
464
532
|
parser = argparse.ArgumentParser(
|
|
465
533
|
prog="browsertools.py",
|
|
466
|
-
description="Chrome DevTools MCP wrapper -
|
|
534
|
+
description="Chrome DevTools MCP wrapper - multi-instance daemon mode",
|
|
467
535
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
468
536
|
epilog=r"""
|
|
469
537
|
browsertools.py - Chrome DevTools automation tool
|
|
470
538
|
|
|
471
|
-
|
|
472
539
|
Usage: uv run .agents/tools/chrome-devtools/browsertools.py <command>
|
|
473
540
|
|
|
474
541
|
DAEMON MANAGEMENT:
|
|
475
|
-
daemon start Start
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
daemon stop Stop daemon, kill Chrome cleanly
|
|
479
|
-
daemon status Check if daemon running
|
|
542
|
+
daemon start Start new daemon instance
|
|
543
|
+
Returns: "browsertools daemon started. Instance ID: <id>"
|
|
544
|
+
daemon stop <id> Stop daemon. kill chrome cleanly.
|
|
480
545
|
daemon config Create config file
|
|
481
546
|
|
|
547
|
+
ALL OTHER COMMANDS REQUIRE --instance <id>:
|
|
548
|
+
--instance <id> Required for all browser commands
|
|
549
|
+
|
|
482
550
|
NAVIGATION:
|
|
483
551
|
nav <url> Navigate to URL
|
|
484
552
|
--timeout <ms> Max wait time in milliseconds
|
|
@@ -491,37 +559,25 @@ INSPECTION:
|
|
|
491
559
|
shot [path] Screenshot viewport (optional: save to path)
|
|
492
560
|
eval <js> Execute JavaScript, returns JSON
|
|
493
561
|
--args <json> JSON array of element references (advanced)
|
|
494
|
-
Simple: "() => document.title"
|
|
495
|
-
Complex: "(el) => el.innerText" --args '[{"uid":"1_23"}]'
|
|
496
562
|
|
|
497
563
|
INTERACTION:
|
|
498
564
|
click <uid> Click element by UID from snapshot
|
|
499
565
|
fill <uid> <value> Fill input/textarea or select option
|
|
500
|
-
key <key> Press key or combo
|
|
501
|
-
Examples: "Enter", "Tab", "Control+A"
|
|
566
|
+
key <key> Press key or combo (e.g., "Enter", "Tab", "Control+A")
|
|
502
567
|
hover <uid> Hover over element
|
|
503
568
|
drag <from> <to> Drag element from UID to target UID
|
|
504
|
-
fillform <json> Fill multiple form fields
|
|
505
|
-
Example: '[{"uid":"1_1","value":"text"}]'
|
|
569
|
+
fillform <json> Fill multiple form fields
|
|
506
570
|
|
|
507
571
|
DEBUGGING:
|
|
508
|
-
conslist List console messages
|
|
509
|
-
--types error warn Filter by type
|
|
510
|
-
--size N Max messages
|
|
511
|
-
|
|
572
|
+
conslist List console messages (--types, --size)
|
|
512
573
|
consget <msgid> Get console message details
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
--types xhr fetch Filter by resource type
|
|
516
|
-
--size N Max requests
|
|
517
|
-
|
|
518
|
-
netget [reqid] Get request details (omit reqid for currently selected)
|
|
574
|
+
netlist List network requests (--types, --size)
|
|
575
|
+
netget [reqid] Get request details
|
|
519
576
|
|
|
520
577
|
PAGE MANIPULATION:
|
|
521
|
-
resize <
|
|
522
|
-
dialog <
|
|
523
|
-
|
|
524
|
-
upload <uid> <file_path> Upload file
|
|
578
|
+
resize <w> <h> Resize viewport
|
|
579
|
+
dialog <action> Handle alert/confirm/prompt (accept|dismiss)
|
|
580
|
+
upload <uid> <path> Upload file
|
|
525
581
|
|
|
526
582
|
USAGE TIPS:
|
|
527
583
|
**Snapshots** are verbose. Always try to pipe grep first
|
|
@@ -535,31 +591,34 @@ Limit output for **debugging** commands
|
|
|
535
591
|
- Console: conslist --size 5 --types error
|
|
536
592
|
|
|
537
593
|
EXAMPLES:
|
|
538
|
-
#
|
|
539
|
-
uv run .agents/tools/chrome-devtools/browsertools.py daemon start
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
uv run .agents/tools/chrome-devtools/browsertools.py
|
|
544
|
-
uv run .agents/tools/chrome-devtools/browsertools.py snap | grep -iC5 "
|
|
545
|
-
uv run .agents/tools/chrome-devtools/browsertools.py
|
|
546
|
-
uv run .agents/tools/chrome-devtools/browsertools.py
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
- Always stop daemon when you are finished to clean up browser resources
|
|
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
|
|
552
607
|
"""
|
|
553
608
|
)
|
|
554
609
|
|
|
610
|
+
# Global --instance argument
|
|
611
|
+
parser.add_argument("--instance", "-i", help="Daemon instance ID (required for browser commands)")
|
|
612
|
+
|
|
555
613
|
subparsers = parser.add_subparsers(dest="cmd", required=True)
|
|
556
614
|
|
|
557
615
|
# Daemon management
|
|
558
616
|
daemon = subparsers.add_parser("daemon", help="Manage daemon")
|
|
559
617
|
daemon_sub = daemon.add_subparsers(dest="daemon_cmd", required=True)
|
|
560
|
-
daemon_sub.add_parser("start", help="Start daemon")
|
|
561
|
-
daemon_sub.add_parser("stop", help="Stop daemon")
|
|
562
|
-
|
|
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
|
|
563
622
|
daemon_sub.add_parser("config", help="Create default config file")
|
|
564
623
|
|
|
565
624
|
# Browser commands
|
|
@@ -636,37 +695,81 @@ IMPORTANT:
|
|
|
636
695
|
# Handle daemon commands
|
|
637
696
|
if args.cmd == "daemon":
|
|
638
697
|
if args.daemon_cmd == "start":
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
|
643
713
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
sys.
|
|
649
|
-
|
|
650
|
-
|
|
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)
|
|
651
729
|
|
|
652
|
-
elif args.daemon_cmd == "
|
|
653
|
-
if
|
|
654
|
-
|
|
655
|
-
|
|
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)
|
|
656
750
|
else:
|
|
657
|
-
print("
|
|
751
|
+
print("Error: instance_id required. Usage: daemon stop <instance_id>", file=sys.stderr)
|
|
752
|
+
sys.exit(1)
|
|
658
753
|
|
|
659
754
|
elif args.daemon_cmd == "config":
|
|
660
755
|
save_default_config()
|
|
661
|
-
print(f"\nEdit {CONFIG_FILE} to customize MCP server settings.")
|
|
662
|
-
print("\nExamples:")
|
|
663
|
-
print(" Headless mode: --headless=true")
|
|
664
|
-
print(" Custom Chrome: --executablePath=/path/to/chrome")
|
|
665
|
-
print(" Sandbox: --headless=true --executablePath=/usr/local/bin/chromium-mcp")
|
|
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)
|
|
666
760
|
|
|
667
761
|
return
|
|
668
762
|
|
|
669
|
-
# Execute browser command
|
|
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
|
+
|
|
670
773
|
try:
|
|
671
774
|
cmd_args_dict = {}
|
|
672
775
|
|
|
@@ -724,7 +827,7 @@ IMPORTANT:
|
|
|
724
827
|
elif args.cmd == "fillform":
|
|
725
828
|
cmd_args_dict["elements"] = json.loads(args.elements)
|
|
726
829
|
|
|
727
|
-
output = await execute_command(args.cmd, cmd_args_dict)
|
|
830
|
+
output = await execute_command(args.instance, args.cmd, cmd_args_dict)
|
|
728
831
|
print(output)
|
|
729
832
|
|
|
730
833
|
except Exception as e:
|
|
@@ -15,6 +15,7 @@ WAIT: Poll the mcp__annotated-feedback__list endpoint for feedback with status:
|
|
|
15
15
|
- navigate to the dev server (see `.agents/repo.md`)
|
|
16
16
|
- manually execute user flows impacted by your change
|
|
17
17
|
- screenshot at each checkpoint, and PONDER visual issues and allignment with expectaions.
|
|
18
|
+
- check browser and dev server logs for errors
|
|
18
19
|
4. Itterate, refine, UAT loop until satisfied.
|
|
19
20
|
5. DEPLOY: run Pre-deployment validation build eg. `vercel build --yes`. when green, commit and push changes to kick off CI/CD. Mission accomplished!
|
|
20
21
|
[/WORKFLOW]
|
package/.claude/commands/cook.md
CHANGED
|
@@ -18,7 +18,7 @@ Populate your initial Todos with your step by step WORKFLOW:
|
|
|
18
18
|
|
|
19
19
|
[WORKFLOW]
|
|
20
20
|
1. PLAN: ULTRATHINK and break down ASSIGNMENT into independent implementation tasks
|
|
21
|
-
2. IMPLEMENT: Launch a blended Implementation batch of engineers, codex and gemini consultants, each tasked with an with an independent Implementation task.
|
|
21
|
+
2. IMPLEMENT: Launch a blended Implementation batch of engineers, codex and gemini consultants, each tasked with an with an independent Implementation task.
|
|
22
22
|
3. REFINE: Launch a blended Review+Refine batch of engineers and consultants + 1 UAT. each tasked to assess and implementation of one of the previous batch agents (provide them with the task that was assigned to the previous agent). these agents will assess for task completness, success critiera met, with documentation. They should then refine the implementation if needed. this batch should be alternate the previous batch. eg. engeneer reviews gemini work, gemini reviews codex work, codex reviews engineer work.
|
|
23
23
|
4. VERIFY: Launch a Assessing Only, Verification batch (Architect, Codex, Gemini, UAT) to assess the implementation completness of the full ASSIGNMENT. Each agent in this batch will provide their unique perspective, so give them the same comprehensive assessment task
|
|
24
24
|
5...N. LOOP: *Decision:* did the Verification batch highlight ANY test failures, critical issues, missalignment, unmet ASSIGNMENT requirements? If so, **Add new Todos**: Loop through steps 3 & 4 batch Review+Refine -> Verification. Continue itterating until 100% pass/green/success reported by Verification batch
|
|
@@ -63,7 +63,7 @@ TEAM COLLABORATION:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
⭐*The successful delivery of your assigned task, contributes to the high level Assignment:*⭐
|
|
66
|
-
[
|
|
66
|
+
[Your full ASSIGNMENT]
|
|
67
67
|
|
|
68
68
|
⭐Ensure you are alligned with this North Star objective*⭐
|
|
69
69
|
|
|
@@ -79,7 +79,7 @@ Don't refer to prior batch agents by name. Instead, supply reference artifacts t
|
|
|
79
79
|
### Batch Composition Goals
|
|
80
80
|
- **Minimum**: 5 agents per implementation batch
|
|
81
81
|
- **Target**: 8-10 agents including support roles
|
|
82
|
-
- **Include**:
|
|
82
|
+
- **Include**: roughly equal number of gemini, codex, and engineer implementers, and a support architect for guidance/coordination.
|
|
83
83
|
- **Follow with**: Verification batch always
|
|
84
84
|
|
|
85
85
|
### Phase Management
|
|
@@ -92,7 +92,7 @@ Don't refer to prior batch agents by name. Instead, supply reference artifacts t
|
|
|
92
92
|
|
|
93
93
|
## Consultant Agent
|
|
94
94
|
Consultants come in two variants: Codex and Gemini. leverage them both liberally to diversify.
|
|
95
|
-
|
|
95
|
+
For implementation batches, distribute the tasks across engineers, codex consultants, and gemini consultants. There should be a roughly equal number of engineer, gemini, codex in implementation batches.
|
|
96
96
|
When you are tasking an agent with read+document type task, recruit a codex and gemini Consultant with the exact same instructions (except a different names). Include the consultants in the same batch. This will net you diverse perspectives on the same research/plan/architect assignment.
|
|
97
97
|
|
|
98
98
|
|
package/CLAUDE.md
CHANGED
|
@@ -83,7 +83,7 @@ TEAM COLLABORATION:
|
|
|
83
83
|
- All agents in a batch complete independently without waiting for others
|
|
84
84
|
|
|
85
85
|
Parallel Specialization Examples
|
|
86
|
-
- **Multiple Engineers
|
|
86
|
+
- **Multiple Engineers+Consultants**: Each owns different module/file
|
|
87
87
|
- **Support Roles**: Architect answers questions or researches and suggests solution approachs, Designer creates storybook assets, UAT manually performs user flows and provides feedback
|
|
88
88
|
|
|
89
89
|
### Inter-Batch Sequencing (Dependencies)
|
|
@@ -124,8 +124,8 @@ Research/Architecture → Planning → Implementation → Verification → Next
|
|
|
124
124
|
#### New Feature Workflow
|
|
125
125
|
1. **Discovery Batch**: [Architect, Designer, Consutant]
|
|
126
126
|
2. **Planning Batch**: [Planner] - creates phase-id and WPs
|
|
127
|
-
3. **Implementation Batch**: [
|
|
128
|
-
4. **Review+Refine Batch**: [
|
|
127
|
+
3. **Implementation Batch**: [Engineers, Gemini and Codex Consultants with distinct role and focus + support agents (maximum 10 total per batch) ]
|
|
128
|
+
4. **Review+Refine Batch**: [Engineers, Codex and Gemini Consultants with previous batch task (alternate Engineers and Consultants so they review/refine each others work) to review+refine + support agents (maximum 10 total per batch) ]
|
|
129
129
|
5. **Verification + Reco Batch**: [Codex + Gemini + Architect + UAT to provide their completion assessments]
|
|
130
130
|
6. **If verificaiton fail**: Loop Review+refine -> Verification Batches until PASS
|
|
131
131
|
|
|
@@ -167,7 +167,7 @@ Research/Architecture → Planning → Implementation → Verification → Next
|
|
|
167
167
|
|
|
168
168
|
## Consultant Agent
|
|
169
169
|
Consultants come in two variants: Codex and Gemini. leverage them both liberally to diversify.
|
|
170
|
-
|
|
170
|
+
For implementation batches, distribute the tasks across engineers, codex consultants, and gemini consultants. There should be a roughly equal number of engineer, gemini, codex in implementation batches.
|
|
171
171
|
When you are tasking an agent with read+document type task, recruit a codex and gemini Consultant with the exact same instructions (except a different names). Include the consultants in the same batch. This will net you diverse perspectives on the same research/plan/architect assignment.
|
|
172
172
|
|
|
173
173
|
|
package/package.json
CHANGED
|
@@ -334,13 +334,30 @@ class Installer extends EventEmitter {
|
|
|
334
334
|
// Create backups if files exist
|
|
335
335
|
await this._createBackupsIfNeeded(fetchedFiles);
|
|
336
336
|
|
|
337
|
-
//
|
|
337
|
+
// Flatten files and filter out user-customizable files if they already exist
|
|
338
|
+
let filesToWrite = this._flattenFiles(fetchedFiles);
|
|
339
|
+
|
|
340
|
+
// Files that should not be overwritten if they exist (user customizations)
|
|
341
|
+
const preserveIfExists = [
|
|
342
|
+
{ path: '.agents/repo.md', fullPath: path.join(this.options.targetDir, '.agents', 'repo.md') },
|
|
343
|
+
{ path: '.agents/tools/chrome-devtools/config.json', fullPath: path.join(this.options.targetDir, '.agents', 'tools', 'chrome-devtools', 'config.json') }
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
for (const file of preserveIfExists) {
|
|
347
|
+
const exists = await this.validator.pathExists(file.fullPath);
|
|
348
|
+
if (exists) {
|
|
349
|
+
this.logger.info(`${file.path} already exists, skipping (preserving user customizations)`);
|
|
350
|
+
filesToWrite = filesToWrite.filter(f => f.path !== file.path);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Write files using injected writer (overwrite existing, no backups)
|
|
338
355
|
const writeResult = await this.writer.writeDirectory(
|
|
339
|
-
|
|
356
|
+
filesToWrite,
|
|
340
357
|
{
|
|
341
358
|
targetDir: this.options.targetDir,
|
|
342
|
-
overwrite:
|
|
343
|
-
backup:
|
|
359
|
+
overwrite: true,
|
|
360
|
+
backup: false,
|
|
344
361
|
preservePermissions: true,
|
|
345
362
|
dryRun: this.options.dryRun
|
|
346
363
|
}
|
|
@@ -450,6 +467,11 @@ class Installer extends EventEmitter {
|
|
|
450
467
|
this.state = InstallationState.VERIFYING;
|
|
451
468
|
this._emitProgress('Verifying installation...', 90);
|
|
452
469
|
|
|
470
|
+
if (this.options.dryRun) {
|
|
471
|
+
this.logger.info('DRY RUN: Would verify installation');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
453
475
|
const requiredPaths = [
|
|
454
476
|
path.join(this.options.targetDir, '.claude'),
|
|
455
477
|
path.join(this.options.targetDir, '.agents'),
|
|
@@ -706,29 +728,27 @@ class Installer extends EventEmitter {
|
|
|
706
728
|
}
|
|
707
729
|
|
|
708
730
|
/**
|
|
709
|
-
*
|
|
731
|
+
* Log info about existing files that will be overwritten
|
|
710
732
|
*/
|
|
711
733
|
async _createBackupsIfNeeded(_fetchedFiles) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
734
|
+
// Check for existing directories and files that will be overwritten
|
|
735
|
+
const claudeDir = path.join(this.options.targetDir, '.claude');
|
|
736
|
+
const agentsDir = path.join(this.options.targetDir, '.agents');
|
|
737
|
+
const claudeMd = path.join(this.options.targetDir, 'CLAUDE.md');
|
|
738
|
+
|
|
739
|
+
const existingPaths = [];
|
|
740
|
+
if (await this.validator.pathExists(claudeDir)) {
|
|
741
|
+
existingPaths.push('.claude/');
|
|
742
|
+
}
|
|
743
|
+
if (await this.validator.pathExists(agentsDir)) {
|
|
744
|
+
existingPaths.push('.agents/');
|
|
745
|
+
}
|
|
746
|
+
if (await this.validator.pathExists(claudeMd)) {
|
|
747
|
+
existingPaths.push('CLAUDE.md');
|
|
748
|
+
}
|
|
728
749
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
750
|
+
if (existingPaths.length > 0) {
|
|
751
|
+
this.logger.info(`Existing files will be overwritten: ${existingPaths.join(', ')}`);
|
|
732
752
|
}
|
|
733
753
|
}
|
|
734
754
|
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
Meta Agent Assignment:
|
|
2
|
-
#$ARGUMENTS
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Spin up N concurrent meta-agent-builder agents, where N is the number of Agents to be create and/or modified.
|
|
8
|
-
Task each meta-agent-builder with a single agent to create/update
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
REMEMBER: the meta-agent-builder is building the agent, not tasking the agent. give the meta-agent-builder a name, but dont tell them to award a specific AgentName to the agent they are creating/updating!
|