@kokorolx/ai-sandbox-wrapper 3.3.0 → 3.4.1
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/bin/ai-run +39 -7
- package/lib/playwright-mcp-config.sh +80 -0
- package/package.json +1 -1
package/bin/ai-run
CHANGED
|
@@ -791,6 +791,7 @@ fi
|
|
|
791
791
|
HOST_CHROME_CDP=false
|
|
792
792
|
HOST_CHROME_CDP_PORT=19222
|
|
793
793
|
PLAYWRIGHT_MCP_NAME=""
|
|
794
|
+
CHROME_DEVTOOLS_MCP_NAME=""
|
|
794
795
|
# Portable symlink-resolving SCRIPT_DIR (macOS readlink has no -f).
|
|
795
796
|
# Needed because ai-run is typically invoked via the ~/bin/ai-run symlink.
|
|
796
797
|
_pmcp_resolve_script_dir() {
|
|
@@ -822,7 +823,17 @@ if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBO
|
|
|
822
823
|
# Deterministic port per container name
|
|
823
824
|
CONTAINER_HASH=$(echo "$CONTAINER_NAME_VALUE" | md5sum | cut -c1-4)
|
|
824
825
|
HOST_CHROME_CDP_PORT=$((19222 + 0x$CONTAINER_HASH % 100))
|
|
825
|
-
|
|
826
|
+
|
|
827
|
+
# Only register entries for MCP binaries actually present in the image
|
|
828
|
+
# (tracked in $AI_SANDBOX_CONFIG by setup.sh). Otherwise opencode would
|
|
829
|
+
# try to spawn a missing binary and fail. (is_mcp_installed is defined
|
|
830
|
+
# later in the file, so use jq inline here.)
|
|
831
|
+
if jq -e '.mcp.installed // [] | index("playwright") != null' "$AI_SANDBOX_CONFIG" &>/dev/null; then
|
|
832
|
+
PLAYWRIGHT_MCP_NAME="playwright_port_${HOST_CHROME_CDP_PORT}"
|
|
833
|
+
fi
|
|
834
|
+
if jq -e '.mcp.installed // [] | index("chrome-devtools") != null' "$AI_SANDBOX_CONFIG" &>/dev/null; then
|
|
835
|
+
CHROME_DEVTOOLS_MCP_NAME="chrome-devtools_port_${HOST_CHROME_CDP_PORT}"
|
|
836
|
+
fi
|
|
826
837
|
|
|
827
838
|
# Reuse-if-alive: probe before launching
|
|
828
839
|
if pmcp::probe_chrome "$HOST_CHROME_CDP_PORT"; then
|
|
@@ -850,23 +861,29 @@ if [[ "$TOOL" == "opencode" ]] && command -v jq &>/dev/null && [[ -f "$AI_SANDBO
|
|
|
850
861
|
HOST_CHROME_CDP=false
|
|
851
862
|
kill "$CHROME_PID" 2>/dev/null || true
|
|
852
863
|
PLAYWRIGHT_MCP_NAME=""
|
|
864
|
+
CHROME_DEVTOOLS_MCP_NAME=""
|
|
853
865
|
fi
|
|
854
866
|
fi
|
|
855
867
|
|
|
856
|
-
# Locked sweep+
|
|
868
|
+
# Locked sweep+register on the shared OpenCode config — both
|
|
869
|
+
# playwright-mcp and chrome-devtools-mcp point at the same host Chrome.
|
|
857
870
|
if [[ "$HOST_CHROME_CDP" == "true" ]]; then
|
|
858
871
|
OPENCODE_CONFIG_FILE="$HOME/.config/opencode/opencode.json"
|
|
859
872
|
LOCK_FILE="$HOME/.config/opencode/.playwright.lock"
|
|
860
873
|
mkdir -p "$(dirname "$OPENCODE_CONFIG_FILE")"
|
|
861
874
|
[[ -f "$OPENCODE_CONFIG_FILE" ]] || echo '{}' > "$OPENCODE_CONFIG_FILE"
|
|
862
|
-
pmcp::with_lock "$LOCK_FILE" pmcp::
|
|
875
|
+
pmcp::with_lock "$LOCK_FILE" pmcp::register_host_chrome \
|
|
876
|
+
"$OPENCODE_CONFIG_FILE" "$HOST_CHROME_CDP_PORT" \
|
|
877
|
+
"$PLAYWRIGHT_MCP_NAME" "$CHROME_DEVTOOLS_MCP_NAME"
|
|
863
878
|
rc=$?
|
|
864
879
|
if [[ "$rc" == "99" ]]; then
|
|
865
880
|
echo " ⚠️ Could not acquire MCP config lock within 5s; skipping registration."
|
|
866
881
|
PLAYWRIGHT_MCP_NAME=""
|
|
882
|
+
CHROME_DEVTOOLS_MCP_NAME=""
|
|
867
883
|
elif [[ "$rc" != "0" ]]; then
|
|
868
884
|
echo " ⚠️ MCP config update failed (rc=$rc); skipping registration."
|
|
869
885
|
PLAYWRIGHT_MCP_NAME=""
|
|
886
|
+
CHROME_DEVTOOLS_MCP_NAME=""
|
|
870
887
|
fi
|
|
871
888
|
fi
|
|
872
889
|
fi
|
|
@@ -2103,9 +2120,16 @@ configure_opencode_mcp() {
|
|
|
2103
2120
|
for tool in "${all_tools[@]}"; do
|
|
2104
2121
|
case "$tool" in
|
|
2105
2122
|
chrome-devtools)
|
|
2106
|
-
|
|
2107
|
-
|
|
2123
|
+
# Skip static entry under host Chrome mode — per-container
|
|
2124
|
+
# chrome-devtools_port_<port> entry is registered at runtime.
|
|
2125
|
+
if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
|
|
2126
|
+
echo " ℹ️ Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
|
|
2108
2127
|
configured_any=true
|
|
2128
|
+
else
|
|
2129
|
+
if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
|
|
2130
|
+
echo " ✓ Configured Chrome DevTools MCP"
|
|
2131
|
+
configured_any=true
|
|
2132
|
+
fi
|
|
2109
2133
|
fi
|
|
2110
2134
|
;;
|
|
2111
2135
|
playwright)
|
|
@@ -2147,8 +2171,13 @@ configure_opencode_mcp() {
|
|
|
2147
2171
|
if [[ "$tool_choice" =~ ^[Yy]$ ]]; then
|
|
2148
2172
|
case "$tool" in
|
|
2149
2173
|
chrome-devtools)
|
|
2150
|
-
if
|
|
2151
|
-
echo "
|
|
2174
|
+
if [[ -n "$PLAYWRIGHT_HOST_CHROME" ]] && [[ -f "$PLAYWRIGHT_HOST_CHROME" ]]; then
|
|
2175
|
+
echo " ℹ️ Chrome DevTools MCP entry will be registered per-container at runtime (host Chrome mode)."
|
|
2176
|
+
configured_any=true
|
|
2177
|
+
else
|
|
2178
|
+
if add_mcp_config "chrome-devtools" "[\"chrome-devtools-mcp\", \"--headless\", \"--isolated\", \"--executablePath\", \"$CHROMIUM_PATH\", \"--chrome-arg=--no-sandbox\"]"; then
|
|
2179
|
+
echo " ✓ Configured"
|
|
2180
|
+
fi
|
|
2152
2181
|
fi
|
|
2153
2182
|
;;
|
|
2154
2183
|
playwright)
|
|
@@ -2726,6 +2755,9 @@ if [[ -n "${PLAYWRIGHT_MCP_NAME:-}" ]]; then
|
|
|
2726
2755
|
DOCKER_ARGS+=(-e "PLAYWRIGHT_MCP_NAME=$PLAYWRIGHT_MCP_NAME")
|
|
2727
2756
|
DOCKER_ARGS+=(-e "PLAYWRIGHT_PORT=$HOST_CHROME_CDP_PORT")
|
|
2728
2757
|
fi
|
|
2758
|
+
if [[ -n "${CHROME_DEVTOOLS_MCP_NAME:-}" ]]; then
|
|
2759
|
+
DOCKER_ARGS+=(-e "CHROME_DEVTOOLS_MCP_NAME=$CHROME_DEVTOOLS_MCP_NAME")
|
|
2760
|
+
fi
|
|
2729
2761
|
DOCKER_ARGS+=($TERMINAL_SIZE)
|
|
2730
2762
|
|
|
2731
2763
|
DOCKER_ARGS+=("$IMAGE")
|
|
@@ -29,6 +29,86 @@ pmcp::probe_chrome() {
|
|
|
29
29
|
# Host address from container (Docker Desktop on Mac).
|
|
30
30
|
PMCP_DOCKER_HOST_IP="${PMCP_DOCKER_HOST_IP:-192.168.65.254}"
|
|
31
31
|
|
|
32
|
+
# Probe and remove .mcp.<prefix>* entries whose CDP port is no longer alive.
|
|
33
|
+
# Used to garbage-collect stale per-container Chrome MCP entries.
|
|
34
|
+
# Args: $1 = config file path, $2 = key prefix (e.g. "playwright_", "chrome-devtools_")
|
|
35
|
+
pmcp::sweep_dead() {
|
|
36
|
+
local cfg="$1" prefix="$2"
|
|
37
|
+
[[ -f "$cfg" ]] || return 0
|
|
38
|
+
|
|
39
|
+
local keys dead_keys=()
|
|
40
|
+
keys=$(jq -r --arg p "$prefix" '(.mcp // {}) | keys[] | select(startswith($p))' "$cfg" 2>/dev/null || true)
|
|
41
|
+
while IFS= read -r key; do
|
|
42
|
+
[[ -z "$key" ]] && continue
|
|
43
|
+
# Find an http:// or ws:// URL in the command — any flag name works.
|
|
44
|
+
local cmd_url
|
|
45
|
+
cmd_url=$(jq -r --arg k "$key" \
|
|
46
|
+
'.mcp[$k].command[]? | select(startswith("http://") or startswith("ws://"))' \
|
|
47
|
+
"$cfg" 2>/dev/null | head -1)
|
|
48
|
+
[[ -z "$cmd_url" ]] && continue
|
|
49
|
+
# Extract port from http://host:port[/path] or ws://host:port[/path]
|
|
50
|
+
local hostport="${cmd_url#*://}" # host:port[/path]
|
|
51
|
+
hostport="${hostport%%/*}" # host:port
|
|
52
|
+
local entry_port="${hostport##*:}" # port
|
|
53
|
+
if ! pmcp::probe_chrome "$entry_port"; then
|
|
54
|
+
dead_keys+=("$key")
|
|
55
|
+
fi
|
|
56
|
+
done <<< "$keys"
|
|
57
|
+
|
|
58
|
+
(( ${#dead_keys[@]} == 0 )) && return 0
|
|
59
|
+
|
|
60
|
+
local tmp="$cfg.tmp.$$"
|
|
61
|
+
local args=()
|
|
62
|
+
local i=0
|
|
63
|
+
for k in "${dead_keys[@]+"${dead_keys[@]}"}"; do
|
|
64
|
+
args+=(--arg "k$i" "$k")
|
|
65
|
+
i=$((i + 1))
|
|
66
|
+
done
|
|
67
|
+
jq "${args[@]+"${args[@]}"}" \
|
|
68
|
+
'reduce ([$ARGS.named | to_entries[] | select(.key|startswith("k")) | .value][]) as $k (.; del(.mcp[$k]))' \
|
|
69
|
+
"$cfg" > "$tmp"
|
|
70
|
+
mv "$tmp" "$cfg"
|
|
71
|
+
chmod 600 "$cfg"
|
|
72
|
+
|
|
73
|
+
echo " 🧹 pmcp: removed ${#dead_keys[@]} stale ${prefix}* entr$([ ${#dead_keys[@]} -eq 1 ] && echo y || echo ies): ${dead_keys[*]}"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Register (or overwrite) an MCP entry.
|
|
77
|
+
# Args: $1 = config file path, $2 = key, $3 = command as JSON array string
|
|
78
|
+
# e.g. pmcp::register cfg playwright_port_19222 '["playwright-mcp","--cdp-endpoint","http://192.168.65.254:19222"]'
|
|
79
|
+
pmcp::register() {
|
|
80
|
+
local cfg="$1" key="$2" cmd_json="$3"
|
|
81
|
+
[[ -f "$cfg" ]] || echo '{}' > "$cfg"
|
|
82
|
+
local tmp="$cfg.tmp.$$"
|
|
83
|
+
jq --arg key "$key" --argjson cmd "$cmd_json" \
|
|
84
|
+
'.mcp = (.mcp // {}) | .mcp[$key] = {"type":"local","command":$cmd}' \
|
|
85
|
+
"$cfg" > "$tmp"
|
|
86
|
+
mv "$tmp" "$cfg"
|
|
87
|
+
chmod 600 "$cfg"
|
|
88
|
+
echo " ➕ pmcp: registered $key"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Register playwright-mcp and/or chrome-devtools-mcp for a host Chrome on
|
|
92
|
+
# a given CDP port. Sweeps dead entries of both prefixes first, then writes
|
|
93
|
+
# the requested entries. Pass empty string for a key to skip that one.
|
|
94
|
+
# MUST be called inside a flock.
|
|
95
|
+
# Args: $1 = cfg, $2 = port, $3 = playwright key (or ""), $4 = chrome-devtools key (or "")
|
|
96
|
+
pmcp::register_host_chrome() {
|
|
97
|
+
local cfg="$1" port="$2" pw_key="$3" cd_key="$4"
|
|
98
|
+
local url="http://$PMCP_DOCKER_HOST_IP:$port"
|
|
99
|
+
|
|
100
|
+
pmcp::sweep_dead "$cfg" "playwright_"
|
|
101
|
+
pmcp::sweep_dead "$cfg" "chrome-devtools_"
|
|
102
|
+
if [[ -n "$pw_key" ]]; then
|
|
103
|
+
pmcp::register "$cfg" "$pw_key" \
|
|
104
|
+
"[\"playwright-mcp\",\"--cdp-endpoint\",\"$url\"]"
|
|
105
|
+
fi
|
|
106
|
+
if [[ -n "$cd_key" ]]; then
|
|
107
|
+
pmcp::register "$cfg" "$cd_key" \
|
|
108
|
+
"[\"chrome-devtools-mcp\",\"--browserUrl\",\"$url\"]"
|
|
109
|
+
fi
|
|
110
|
+
}
|
|
111
|
+
|
|
32
112
|
# Sweep dead playwright_* entries and append a new one. MUST be called inside
|
|
33
113
|
# a flock by the caller. Does not acquire the lock itself, by design — locking
|
|
34
114
|
# happens around a larger critical section in the caller.
|
package/package.json
CHANGED