feat: hermes sshx-link plugin - shared terminal from dashboard

This commit is contained in:
OpenCode Agent 2026-08-17 15:15:05 +00:00
commit 6aaf913366
5 changed files with 395 additions and 0 deletions

106
__init__.py Normal file
View File

@ -0,0 +1,106 @@
"""
sshx-link plugin
================
Downloads sshx, starts a shared terminal session,
and returns the shareable link.
"""
import subprocess
import threading
import re
import logging
import shutil
import os
logger = logging.getLogger(__name__)
_sshx_process = None
_sshx_link = None
def _find_link_from_output(line: str) -> str | None:
"""Extract sshx link from process output."""
# sshx outputs links like: https://sshx.io/s/xxxxx
match = re.search(r"https://sshx\.io/s/[A-Za-z0-9_-]+", line)
return match.group(0) if match else None
def _ensure_sshx_installed() -> bool:
"""Download and install sshx binary if not present."""
if shutil.which("sshx"):
logger.info("sshx already installed at %s", shutil.which("sshx"))
return True
logger.info("Downloading sshx...")
try:
result = subprocess.run(
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
logger.error("sshx install failed: %s", result.stderr)
return False
return True
except subprocess.TimeoutExpired:
logger.error("sshx install timed out")
return False
def _run_sshx(tool_ctx):
"""Run sshx serve in background, capture the link."""
global _sshx_process, _sshx_link
if _sshx_process and _sshx_process.poll() is None:
# Already running
tool_ctx.yield_result({"status": "already_running", "link": _sshx_link})
return
if not _ensure_sshx_installed():
tool_ctx.yield_result({"status": "error", "message": "Failed to install sshx"})
return
logger.info("Starting sshx serve...")
_sshx_process = subprocess.Popen(
["sshx", "serve"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
_sshx_link = None
def _reader():
global _sshx_link
for line in _sshx_process.stdout:
line = line.strip()
logger.debug("sshx: %s", line)
link = _find_link_from_output(line)
if link:
_sshx_link = link
tool_ctx.yield_result({"status": "running", "link": link})
return
# Process ended without outputting a link
tool_ctx.yield_result({"status": "error", "message": "sshx exited without producing a link"})
threading.Thread(target=_reader, daemon=True).start()
def register(ctx):
"""Register the sshx_start tool."""
ctx.register_tool(
name="sshx_start",
toolset="sshx-link",
schema={
"description": "Start a sshx shared terminal session and return the shareable link. The link lets others join your terminal via browser.",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
handler=_run_sshx,
)
logger.info("sshx-link plugin registered")

132
dashboard/dist/index.js vendored Normal file
View File

@ -0,0 +1,132 @@
(function() {
const SDK = window.__HERMES_PLUGIN_SDK__;
const { useState, useEffect, useCallback } = React;
function SSHXPage() {
const [link, setLink] = useState(null);
const [status, setStatus] = useState("idle"); // idle | loading | running | error
const [error, setError] = useState(null);
const startSSHX = useCallback(async () => {
setStatus("loading");
setError(null);
try {
const res = await SDK.fetchJSON("/api/plugins/sshx-link/start");
if (res.link) {
setLink(res.link);
setStatus("running");
} else {
setError(res.message || "No link returned");
setStatus("error");
}
} catch (err) {
setError(err.message || "Failed to start sshx");
setStatus("error");
}
}, []);
const stopSSHX = useCallback(async () => {
try {
await fetch("/api/plugins/sshx-link/stop", { method: "POST" });
setLink(null);
setStatus("idle");
} catch (err) {
setError(err.message);
}
}, []);
const checkStatus = useCallback(async () => {
try {
const res = await SDK.fetchJSON("/api/plugins/sshx-link/status");
if (res.status === "running" && res.link) {
setLink(res.link);
setStatus("running");
}
} catch (_) {}
}, []);
useEffect(() => { checkStatus(); }, [checkStatus]);
return (
<div style={{ padding: "2rem", maxWidth: 600, margin: "0 auto" }}>
<h1 style={{ fontSize: "1.5rem", fontWeight: 600, marginBottom: "1rem" }}>
SSHX Shared Terminal
</h1>
<p style={{ color: "#888", marginBottom: "1.5rem" }}>
Start a shared terminal session. Others can join via the link.
</p>
{status === "running" && link ? (
<div style={{
background: "#1a1a2e",
border: "1px solid #0f3460",
borderRadius: 8,
padding: "1.5rem",
marginBottom: "1rem"
}}>
<p style={{ color: "#4ade80", fontWeight: 600, marginBottom: "0.5rem" }}>
Running
</p>
<a
href={link}
target="_blank"
rel="noopener noreferrer"
style={{
color: "#60a5fa",
fontSize: "1.1rem",
wordBreak: "break-all",
textDecoration: "underline"
}}
>
{link}
</a>
<div style={{ marginTop: "1rem" }}>
<button
onClick={stopSSHX}
style={{
background: "#dc2626",
color: "#fff",
border: "none",
borderRadius: 6,
padding: "0.5rem 1rem",
cursor: "pointer",
fontWeight: 500
}}
>
Stop
</button>
</div>
</div>
) : (
<button
onClick={startSSHX}
disabled={status === "loading"}
style={{
background: status === "loading" ? "#555" : "#2563eb",
color: "#fff",
border: "none",
borderRadius: 6,
padding: "0.7rem 1.5rem",
cursor: status === "loading" ? "not-allowed" : "pointer",
fontWeight: 500,
fontSize: "1rem"
}}
>
{status === "loading" ? "Starting..." : "Start SSHX"}
</button>
)}
{error && (
<p style={{ color: "#f87171", marginTop: "1rem" }}>
Error: {error}
</p>
)}
</div>
);
}
// Register the tab component
if (window.__HERMES_PLUGINS__) {
window.__HERMES_PLUGINS__.register("sshx-link", SSHXPage);
}
})();

13
dashboard/manifest.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "sshx-link",
"label": "SSHX",
"description": "Start a shared terminal session and get the link",
"icon": "Terminal",
"version": "1.0.0",
"tab": {
"path": "/sshx",
"position": "end"
},
"entry": "dist/index.js",
"api": "plugin_api.py"
}

136
dashboard/plugin_api.py Normal file
View File

@ -0,0 +1,136 @@
"""
sshx-link dashboard backend
===========================
FastAPI routes for starting sshx and returning the share link.
"""
import asyncio
import subprocess
import re
import shutil
import logging
from fastapi import APIRouter
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
router = APIRouter()
# Keep track of running sshx process
_sshx_process = None
_sshx_link = None
def _find_link(text: str) -> str | None:
"""Extract sshx link from output."""
match = re.search(r"https://sshx\.io/s/[A-Za-z0-9_-]+", text)
return match.group(0) if match else None
def _ensure_sshx() -> bool:
"""Install sshx if not present."""
if shutil.which("sshx"):
return True
try:
subprocess.run(
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
capture_output=True,
timeout=60,
)
return shutil.which("sshx") is not None
except Exception as e:
logger.error("sshx install failed: %s", e)
return False
@router.get("/start")
async def start_sshx():
"""Start sshx serve and return the share link."""
global _sshx_process, _sshx_link
# Already running
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
return {"status": "running", "link": _sshx_link}
# Install if needed
if not _ensure_sshx():
return JSONResponse(
status_code=500,
content={"status": "error", "message": "Failed to install sshx"},
)
# Kill old process if any
if _sshx_process and _sshx_process.poll() is None:
_sshx_process.terminate()
_sshx_process.wait(timeout=5)
# Start sshx serve
_sshx_process = subprocess.Popen(
["sshx", "serve"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
_sshx_link = None
# Read output in background thread, await link
loop = asyncio.get_event_loop()
link_future = loop.create_future()
def _reader():
global _sshx_link
for line in _sshx_process.stdout:
line = line.strip()
link = _find_link(line)
if link:
_sshx_link = link
if not link_future.done():
link_future.set_result(link)
return
if not link_future.done():
link_future.set_exception RuntimeError("sshx exited without link")
import threading
threading.Thread(target=_reader, daemon=True).start()
try:
link = await asyncio.wait_for(link_future, timeout=30)
return {"status": "running", "link": link}
except asyncio.TimeoutError:
return JSONResponse(
status_code=504,
content={"status": "error", "message": "sshx did not return a link in time"},
)
except Exception as e:
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)},
)
@router.get("/status")
async def sshx_status():
"""Check if sshx is running and return the link."""
global _sshx_process, _sshx_link
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
return {"status": "running", "link": _sshx_link}
return {"status": "stopped", "link": None}
@router.post("/stop")
async def stop_sshx():
"""Stop the running sshx process."""
global _sshx_process, _sshx_link
if _sshx_process and _sshx_process.poll() is None:
_sshx_process.terminate()
try:
_sshx_process.wait(timeout=5)
except subprocess.TimeoutExpired:
_sshx_process.kill()
_sshx_process = None
_sshx_link = None
return {"status": "stopped"}
return {"status": "already_stopped"}

8
plugin.yaml Normal file
View File

@ -0,0 +1,8 @@
name: sshx-link
version: 1.0.0
description: Start sshx shared terminal and return the share link
author: sisyphus
provides_tools:
- sshx_start
provides_hooks: []
requires_env: []