Skip to content

feat: add browser-cdp skill for CDP-based browser automation#141

Merged
linuxhsj merged 2 commits intolinuxhsj:mainfrom
lucian-why:feat/browser-cdp-skill
Mar 29, 2026
Merged

feat: add browser-cdp skill for CDP-based browser automation#141
linuxhsj merged 2 commits intolinuxhsj:mainfrom
lucian-why:feat/browser-cdp-skill

Conversation

@lucian-why
Copy link
Copy Markdown
Contributor

@lucian-why lucian-why commented Mar 28, 2026

Summary

Adds a new browser-cdp skill that enables agents to control a browser via Chrome DevTools Protocol (CDP) through a lightweight HTTP proxy.

What's included

  • skills/browser-cdp/SKILL.md — Full skill definition with trigger phrases, workflows, and API reference
  • skills/browser-cdp/scripts/cdp_proxy.py — Self-contained CDP proxy server (~430 lines)

Features

  • Full CDP proxy: navigation, JavaScript evaluation, screenshots, clicks, form filling
  • Auto-discovery of Chrome debugging ports (tries multiple known ports)
  • Session management with tab tracking via /targets
  • No external dependencies — Python stdlib only
  • REST-friendly HTTP API wrapping CDP's WebSocket protocol

Validation

Passed quick_validate.py from the skill-creator skill.

Example usage

POST /eval
Body: document.title
→ "My Page"
GET /screenshot?target=page_1
→ PNG image

Summary by CodeRabbit

发布说明

  • 新功能
    • 新增 browser-cdp 技能,支持通过本地 HTTP 代理控制真实浏览器进行导航、截图、执行 JavaScript、点击元素和管理标签页等操作。
  • 文档
    • 提供详尽使用说明、REST 接口说明及常见工作流示例(页面抓取、截图、扩展搜索/安装、表单交互等)。
  • 其它
    • 启动/关闭与错误处理行为在文档中有说明。

Add a new skill that enables agents to control a browser via Chrome DevTools Protocol (CDP) through a lightweight HTTP proxy.

Key features:
- Full CDP proxy server that translates HTTP requests to CDP commands
- Support for navigation, JavaScript evaluation, screenshots, clicks, and more
- Session management with tab tracking and target listing
- Built-in auto-discovery of Chrome debugging ports
- Comprehensive SKILL.md with workflow guides and API reference

Files:
- skills/browser-cdp/SKILL.md: Skill definition with trigger phrases, workflows, and reference
- skills/browser-cdp/scripts/cdp_proxy.py: Self-contained CDP proxy server (~430 lines)
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

引入新的 browser-cdp 技能:添加使用本地运行的 Chrome DevTools Protocol (CDP) HTTP 代理的文档与可执行 Python 代理脚本,暴露导航、截图、JS 执行、元素点击、选项卡管理和 target 列表等 REST 端点。

Changes

Cohort / File(s) Summary
技能文档
skills/browser-cdp/SKILL.md
新增技能文档,说明先决条件、使用/不使用场景、REST 端点规范(/targets/navigate/screenshot/eval/click/new)及示例工作流和注意事项。
CDP 代理脚本
skills/browser-cdp/scripts/cdp_proxy.py
新增可执行 Python HTTP 代理:包含 Chrome 路径检测、空闲端口选择、启动浏览器、CDP 请求轮询、HTTP 路由实现(导航、截图、click、new、POST /eval 等)、错误处理与进程清理。

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Proxy
    participant Browser
    participant CDP

    Client->>Proxy: GET /navigate?url=...
    Proxy->>Browser: 访问 http://localhost:{debug_port}/json 获取 targets
    Browser-->>Proxy: 返回 target 列表
    Proxy->>CDP: Target.sendMessageToTarget (Page.navigate)
    CDP->>Browser: 执行导航
    Browser-->>Proxy: 导航完成事件
    Proxy-->>Client: 返回导航结果

    Client->>Proxy: GET /screenshot?target=...
    Proxy->>CDP: Page.captureScreenshot
    CDP->>Browser: 生成截图
    Browser-->>Proxy: 返回 base64 PNG
    Proxy-->>Client: 返回 image/png 二进制

    Client->>Proxy: POST /eval (JS body)
    Proxy->>CDP: Runtime.evaluate (awaitPromise, returnByValue)
    CDP->>Browser: 执行并解析结果
    Browser-->>Proxy: 返回评估结果
    Proxy-->>Client: 返回 JSON 结果
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 新代理跳出洞,
浏览器随心动,
点击、截图与执行,
小爪儿写入流,
CDP 的路上我欢腾 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning 描述缺少模板中的多个关键部分,包括变更类型、影响范围、安全影响分析、复现步骤、人工验证和风险评估。 补充PR模板中的所有必需部分,特别是变更类型、安全影响评估、验证步骤和风险评估。
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确总结了主要变化——添加一个用于CDP浏览器自动化的新技能。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
skills/browser-cdp/scripts/cdp_proxy.py (4)

27-31: psutil 已导入但从未使用。

脚本在启动时检查 psutil 是否存在,但实际代码中从未使用它。如果确实不需要,应该移除这个依赖;如果计划用于进程管理(如检测浏览器状态),请实现相关功能。

♻️ 移除未使用的依赖
-try:
-    import psutil
-except ImportError:
-    print("[ERROR] psutil is required. Install with: pip install psutil")
-    sys.exit(1)

或者在 start_browser 和进程清理中使用 psutil 来更可靠地管理浏览器进程。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/browser-cdp/scripts/cdp_proxy.py` around lines 27 - 31, The code
imports psutil in a try/except but never uses it; either remove the psutil
import and the ImportError guard to eliminate an unused dependency, or integrate
psutil into browser process management (e.g., inside start_browser and any
process cleanup/terminate functions) to robustly detect and kill child processes
and confirm the browser has exited; update error messaging and tests
accordingly.

159-178: _cdp_send 方法已定义但从未被调用。

这是无用代码(dead code),应该移除以保持代码整洁。

♻️ 移除未使用的方法
-    def _cdp_send(self, target_id: str, method: str, params: dict = None) -> dict:
-        """Send a CDP command and return the result."""
-        url = f"http://127.0.0.1:{self.server.debug_port}/json"
-        payload = {
-            "id": int(time.time() * 1000) % 1000000,
-            "method": method,
-            "params": params or {},
-        }
-
-        req_data = urllib.request.Request(
-            url,
-            data=json.dumps(payload).encode("utf-8"),
-            headers={"Content-Type": "application/json"},
-        )
-
-        try:
-            with urllib.request.urlopen(req_data, timeout=30) as resp:
-                return json.loads(resp.read().decode("utf-8"))
-        except Exception as e:
-            return {"error": str(e)}
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/browser-cdp/scripts/cdp_proxy.py` around lines 159 - 178, The
_cdp_send method is defined but never used; remove the dead method definition
named _cdp_send from the class in cdp_proxy.py, ensuring you also delete any
imports or local variables only required by that method (e.g., urllib, json,
time) if they are no longer referenced elsewhere; keep the rest of the class
unchanged and run tests/lint to confirm no remaining references to _cdp_send.

343-350: Selector 直接插入错误消息可能导致 JS 注入风险。

虽然 json.dumps(selector) 用于 querySelector 参数是安全的,但错误消息中的 {selector} 是直接字符串插值。如果 selector 包含特殊字符(如 '}),可能破坏 JS 语法。

🛡️ 使用安全的错误消息格式
         js = f"""
         (function() {{
             const el = document.querySelector({json.dumps(selector)});
-            if (!el) return {{error: 'Element not found: {selector}'}};
+            if (!el) return {{error: 'Element not found: ' + {json.dumps(selector)}}};
             el.click();
             return {{success: true}};
         }})()
         """.replace("\n", " ")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/browser-cdp/scripts/cdp_proxy.py` around lines 343 - 350, The error
message embeds the raw selector into the JS string causing a JS injection risk;
change the construction of the js variable so the selector is safely serialized
for both querySelector and the error message (use the same json.dumps(selector)
value or otherwise escape/embed it as a JS string) instead of interpolating
{selector} directly; update the js building in cdp_proxy.py (the js variable
where selector is used) so both querySelector({json.dumps(selector)}) and the
returned error use a safe, encoded representation of selector.

421-427: 异常终止时浏览器进程可能成为孤儿进程。

当前仅处理了 KeyboardInterrupt,但如果脚本因其他异常退出(如 serve_forever 内部错误),浏览器进程不会被清理。建议使用 try/finallyatexit 确保进程终止。

♻️ 使用 finally 确保清理
     try:
         server.serve_forever()
     except KeyboardInterrupt:
         print("\n[CDP] Shutting down...")
+    finally:
         server.shutdown()
         proc.terminate()
+        try:
+            proc.wait(timeout=5)
+        except subprocess.TimeoutExpired:
+            proc.kill()
         print("[CDP] Done")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/browser-cdp/scripts/cdp_proxy.py` around lines 421 - 427, The current
try/except only handles KeyboardInterrupt so the browser subprocess (proc) and
server may be left running on other errors; wrap the server.serve_forever() call
in a try/finally (or register an atexit handler) to always perform cleanup: in
the finally block call server.shutdown() and ensure proc is terminated
(proc.terminate(), optionally wait and proc.kill() if still alive), and
log/handle any exceptions—refer to server.serve_forever, server.shutdown, and
proc.terminate to locate and update the cleanup logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/browser-cdp/scripts/cdp_proxy.py`:
- Around line 368-375: The /json/new endpoint URL is built incorrectly in
_handle_new_tab; change the query to include the "url=" parameter and properly
URL-encode the target (e.g., build the URL as .../json/new?url=<encoded URL>)
instead of .../json/new?<URL>. Update the URL construction in _handle_new_tab
(use urllib.parse.quote or urllib.parse.urlencode to encode "about:blank" or any
future target) so the request uses /json/new?url=about:blank and then keep the
existing response parsing and error handling.
- Around line 63-74: The function find_chrome_path iterates candidates and runs
"which"/"where" but returns the candidate name instead of the resolved
executable path; change the successful branch in the loop (where proc.returncode
== 0) to return the command output (proc.stdout.strip()) so you return the
actual filesystem path found by which/where; keep the try/except for
subprocess.TimeoutExpired and FileNotFoundError and the existing use of
variables path, proc, and candidates.
- Around line 361-366: The current double JSON parsing in the try block (resp =
result.get("result", {}"); inner = json.loads(resp.get("result", "{}"));
self._send_json(json.loads(inner.get("result", {}).get("value", "{}")))) assumes
inner.result.value is a JSON string and will raise on dict/None/non-string;
update the parsing in cdp_proxy.py to: extract resp and inner safely, get value
= inner.get("result", {}).get("value"), then if value is None send an empty
object or appropriate error, if isinstance(value, str) attempt json.loads with
try/except, if isinstance(value, (dict, list, bool, int, float)) send it
directly, otherwise coerce to string or wrap it, and preserve the existing
except path (self._send_json({"error": str(e)}, 500)) for unexpected errors;
reference resp, inner, and the _send_json call to locate and replace the logic.
- Around line 159-202: The _cdp_send and _cdp_send_to_target functions are
incorrectly sending CDP commands via HTTP POST to /json; fix them to use the
WebSocket-based CDP flow: query GET /json to obtain the target's
webSocketDebuggerUrl (use target_id to pick the matching entry), open a
WebSocket connection to that webSocketDebuggerUrl, send the CDP message as a
JSON object with a unique "id" and the requested "method"/"params", await and
parse the WebSocket response (handle errors/timeouts), and close the socket; for
_cdp_send_to_target do not POST a Target.sendMessageToTarget via /json but
instead connect to the target's own webSocketDebuggerUrl and send the inner
method directly. Use the functions _cdp_send and _cdp_send_to_target as the
locations to replace the urllib.request logic and ensure proper JSON message
framing and response/error handling over WebSocket.

In `@skills/browser-cdp/SKILL.md`:
- Around line 188-189: Replace the unparsed `{baseDir}` placeholder in the two
bullet lines with the same placeholder syntax used at Line 26 so the variable is
resolved (e.g., change `python3 {baseDir}/scripts/cdp_proxy.py` to use the
parsed form used on Line 26 such as `python3 ${baseDir}/scripts/cdp_proxy.py`),
ensuring both occurrences of `{baseDir}` in the CDP proxy note match the working
interpolation pattern.
- Around line 21-29: The Prerequisites section omits the required Python
dependency psutil referenced by cdp_proxy.py; update the SKILL.md Prerequisites
paragraph to instruct users to install psutil (e.g., via pip) before running
python3 {baseDir}/scripts/cdp_proxy.py, and mention that cdp_proxy.py checks for
the psutil package on startup so the dependency must be installed first.
- Around line 25-27: The SKILL.md command uses the unexpanded placeholder
"{baseDir}" which won't be substituted (per src/agents/system-prompt.ts) and
will break execution; update the SKILL.md entry to either use a relative path
from the skill directory (e.g., replace "{baseDir}/scripts/cdp_proxy.py" with
"skills/browser-cdp/scripts/cdp_proxy.py") or add a clear note for users to
replace {baseDir} with their repository root/absolute path so the python3
command is runnable, ensuring the change targets the line containing the
"{baseDir}" usage in SKILL.md.
- Around line 157-161: The JavaScript payload in the curl command has an extra
closing bracket in the expression passed to JSON.stringify; locate the string
containing JSON.stringify([...document.querySelectorAll('a[data-id]')].map(a =>
({id: a.dataset.id, title: a.textContent.trim()}))]) and remove the stray ']' so
the expression ends with ...trim()})))" (i.e., close the map and JSON.stringify
correctly), ensuring JSON.stringify(... ) wraps the mapped array without the
extra bracket.

---

Nitpick comments:
In `@skills/browser-cdp/scripts/cdp_proxy.py`:
- Around line 27-31: The code imports psutil in a try/except but never uses it;
either remove the psutil import and the ImportError guard to eliminate an unused
dependency, or integrate psutil into browser process management (e.g., inside
start_browser and any process cleanup/terminate functions) to robustly detect
and kill child processes and confirm the browser has exited; update error
messaging and tests accordingly.
- Around line 159-178: The _cdp_send method is defined but never used; remove
the dead method definition named _cdp_send from the class in cdp_proxy.py,
ensuring you also delete any imports or local variables only required by that
method (e.g., urllib, json, time) if they are no longer referenced elsewhere;
keep the rest of the class unchanged and run tests/lint to confirm no remaining
references to _cdp_send.
- Around line 343-350: The error message embeds the raw selector into the JS
string causing a JS injection risk; change the construction of the js variable
so the selector is safely serialized for both querySelector and the error
message (use the same json.dumps(selector) value or otherwise escape/embed it as
a JS string) instead of interpolating {selector} directly; update the js
building in cdp_proxy.py (the js variable where selector is used) so both
querySelector({json.dumps(selector)}) and the returned error use a safe, encoded
representation of selector.
- Around line 421-427: The current try/except only handles KeyboardInterrupt so
the browser subprocess (proc) and server may be left running on other errors;
wrap the server.serve_forever() call in a try/finally (or register an atexit
handler) to always perform cleanup: in the finally block call server.shutdown()
and ensure proc is terminated (proc.terminate(), optionally wait and proc.kill()
if still alive), and log/handle any exceptions—refer to server.serve_forever,
server.shutdown, and proc.terminate to locate and update the cleanup logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bca10005-4f79-4a53-a7bd-8d35baa3411c

📥 Commits

Reviewing files that changed from the base of the PR and between eff1bd4 and 7f2ed1c.

📒 Files selected for processing (2)
  • skills/browser-cdp/SKILL.md
  • skills/browser-cdp/scripts/cdp_proxy.py

Comment on lines +159 to +202
def _cdp_send(self, target_id: str, method: str, params: dict = None) -> dict:
"""Send a CDP command and return the result."""
url = f"http://127.0.0.1:{self.server.debug_port}/json"
payload = {
"id": int(time.time() * 1000) % 1000000,
"method": method,
"params": params or {},
}

req_data = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
)

try:
with urllib.request.urlopen(req_data, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
return {"error": str(e)}

def _cdp_send_to_target(self, target_id: str, method: str, params: dict = None) -> dict:
"""Send a CDP command to a specific target via HTTP."""
url = f"http://127.0.0.1:{self.server.debug_port}/json"
payload = {
"id": int(time.time() * 1000) % 1000000,
"method": "Target.sendMessageToTarget",
"params": {
"targetId": target_id,
"message": json.dumps({"id": 1, "method": method, "params": params or {}}),
},
}

req_data = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
)

try:
with urllib.request.urlopen(req_data, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
return {"error": str(e)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

How to send CDP commands to Chrome browser? Does Chrome /json endpoint accept CDP protocol messages via HTTP POST?

💡 Result:

To send CDP (Chrome DevTools Protocol) commands to a Chrome browser, launch Chrome with remote debugging enabled using the flag --remote-debugging-port=9222 (replace 9222 with your preferred port). This starts an HTTP server on that port. 1. Query http://localhost:9222/json/list (or /json) via GET to get a list of open tabs/targets, each with a webSocketDebuggerUrl like ws://localhost:9222/devtools/page/{targetId}. 2. Connect a WebSocket client to the target's webSocketDebuggerUrl. 3. Send CDP commands as JSON-RPC messages over the WebSocket, e.g.: { "id": 1, "method": "Page.navigate", "params": {"url": "https://example.com"} } Receive responses and events similarly. For programmatic access, use libraries like Puppeteer (page.target.createCDPSession), Chrome Remote Interface, or raw WebSocket. Chrome extensions can use the chrome.debugger API to send commands to tabs. The /json endpoint (and related like /json/version, /json/protocol) only supports specific HTTP methods: GET for listing targets/protocol/version, PUT /json/new for new tabs. It does not accept arbitrary CDP protocol messages via HTTP POST. CDP commands require the bidirectional WebSocket connection after discovering the endpoint via /json. Note: Since Chrome 136 (2025), remote debugging requires --user-data-dir with a non-default profile for security.

Citations:


停止:Chrome /json 端点无法接收 CDP 协议命令。

Chrome DevTools Protocol(CDP)命令必须通过 WebSocket 连接发送,不能通过 HTTP POST 到 /json 端点。/json 端点仅支持 GET(列出 targets)和 PUT(创建新 tab)等 REST 操作,不接受任何 CDP 协议消息。

正确的流程:

  1. 通过 GET /json 获取 targets 列表和 webSocketDebuggerUrl
  2. 使用 WebSocket 连接到目标的 webSocketDebuggerUrl
  3. 发送 CDP 命令(如 Page.navigateRuntime.evaluate 等)到 WebSocket 连接

当前代码(159-202 行)尝试通过 HTTP POST 发送 CDP 命令,这会导致所有核心功能(导航、截图、JS 执行)都无法工作。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/browser-cdp/scripts/cdp_proxy.py` around lines 159 - 202, The
_cdp_send and _cdp_send_to_target functions are incorrectly sending CDP commands
via HTTP POST to /json; fix them to use the WebSocket-based CDP flow: query GET
/json to obtain the target's webSocketDebuggerUrl (use target_id to pick the
matching entry), open a WebSocket connection to that webSocketDebuggerUrl, send
the CDP message as a JSON object with a unique "id" and the requested
"method"/"params", await and parse the WebSocket response (handle
errors/timeouts), and close the socket; for _cdp_send_to_target do not POST a
Target.sendMessageToTarget via /json but instead connect to the target's own
webSocketDebuggerUrl and send the inner method directly. Use the functions
_cdp_send and _cdp_send_to_target as the locations to replace the urllib.request
logic and ensure proper JSON message framing and response/error handling over
WebSocket.

Comment on lines +188 to +189
- The CDP proxy must be running before using any commands
- If the proxy is not running, ask the user to start it: `python3 {baseDir}/scripts/cdp_proxy.py`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

重复的 {baseDir} 问题。

与 Line 26 相同,此处的 {baseDir} 也不会被解析。

📝 建议修改
-- If the proxy is not running, ask the user to start it: `python3 {baseDir}/scripts/cdp_proxy.py`
+- If the proxy is not running, ask the user to start it: `python3 skills/browser-cdp/scripts/cdp_proxy.py`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- The CDP proxy must be running before using any commands
- If the proxy is not running, ask the user to start it: `python3 {baseDir}/scripts/cdp_proxy.py`
- The CDP proxy must be running before using any commands
- If the proxy is not running, ask the user to start it: `python3 skills/browser-cdp/scripts/cdp_proxy.py`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/browser-cdp/SKILL.md` around lines 188 - 189, Replace the unparsed
`{baseDir}` placeholder in the two bullet lines with the same placeholder syntax
used at Line 26 so the variable is resolved (e.g., change `python3
{baseDir}/scripts/cdp_proxy.py` to use the parsed form used on Line 26 such as
`python3 ${baseDir}/scripts/cdp_proxy.py`), ensuring both occurrences of
`{baseDir}` in the CDP proxy note match the working interpolation pattern.

- find_chrome_path: return which/where stdout (actual resolved path)
- _handle_eval: avoid double json.loads; handle non-string values gracefully
- _handle_click: same value parsing fix as _handle_eval
- _handle_new_tab: fix JSON/new URL format (?url=<URL> not ?<URL>)
- SKILL.md: fix {baseDir} placeholder, add psutil prerequisite,
  fix JS syntax error in Chrome extension example
@linuxhsj linuxhsj merged commit 692a548 into linuxhsj:main Mar 29, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants