mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-26 08:05:08 +02:00
feat: add support for MCP service and CLI subcommands
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
389
api.go
Normal file
389
api.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package kvm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func StartAPIServer(port int) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(corsMiddleware())
|
||||
r.Use(apiKeyAuthMiddleware(config.APIKey))
|
||||
|
||||
// Health check (no auth required)
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"device": GetDeviceID(),
|
||||
"version": builtAppVersion,
|
||||
})
|
||||
})
|
||||
|
||||
// API routes
|
||||
api := r.Group("/api/lan")
|
||||
{
|
||||
// Mouse
|
||||
api.POST("/mouse/absolute", handleAPIMouseAbsolute)
|
||||
api.POST("/mouse/relative", handleAPIMouseRelative)
|
||||
api.POST("/mouse/click", handleAPIMouseClick)
|
||||
api.POST("/mouse/scroll", handleAPIMouseScroll)
|
||||
|
||||
// Keyboard
|
||||
api.POST("/keyboard/key", handleAPIKeyboardKey)
|
||||
api.POST("/keyboard/combo", handleAPIKeyboardCombo)
|
||||
api.POST("/keyboard/type", handleAPIKeyboardType)
|
||||
|
||||
// Capture
|
||||
api.GET("/capture", handleAPICapture)
|
||||
|
||||
// Status
|
||||
api.GET("/status", handleAPIStatus)
|
||||
api.GET("/video/state", handleAPIVideoState)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
logger.Info().Str("addr", addr).Msg("Starting LAN API server")
|
||||
if err := r.Run(addr); err != nil {
|
||||
logger.Error().Err(err).Msg("LAN API server failed")
|
||||
}
|
||||
}
|
||||
|
||||
// === Middleware ===
|
||||
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func apiKeyAuthMiddleware(expectedKey string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Skip auth for health endpoint
|
||||
if c.Request.URL.Path == "/health" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Skip auth for localhost
|
||||
remoteAddr := c.Request.RemoteAddr
|
||||
if strings.HasPrefix(remoteAddr, "127.0.0.1:") ||
|
||||
strings.HasPrefix(remoteAddr, "[::1]:") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// If no API key configured, reject LAN requests
|
||||
if expectedKey == "" {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "API key not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
auth := c.GetHeader("Authorization")
|
||||
var key string
|
||||
if _, err := fmt.Sscanf(auth, "Bearer %s", &key); err != nil {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "missing or invalid authorization"})
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(key, expectedKey) {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "invalid api key"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// === API Handlers: Mouse ===
|
||||
|
||||
func handleAPIMouseAbsolute(c *gin.Context) {
|
||||
var req struct {
|
||||
X int `json:"x" binding:"required"`
|
||||
Y int `json:"y" binding:"required"`
|
||||
Buttons uint8 `json:"buttons"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": req.X, "y": req.Y, "buttons": req.Buttons,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "x": req.X, "y": req.Y})
|
||||
}
|
||||
|
||||
func handleAPIMouseRelative(c *gin.Context) {
|
||||
var req struct {
|
||||
Dx int8 `json:"dx" binding:"required"`
|
||||
Dy int8 `json:"dy" binding:"required"`
|
||||
Buttons uint8 `json:"buttons"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["relMouseReport"], map[string]interface{}{
|
||||
"dx": req.Dx, "dy": req.Dy, "buttons": req.Buttons,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "dx": req.Dx, "dy": req.Dy})
|
||||
}
|
||||
|
||||
func handleAPIMouseClick(c *gin.Context) {
|
||||
var req struct {
|
||||
Button string `json:"button" binding:"required"`
|
||||
Double bool `json:"double"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var buttons uint8
|
||||
switch req.Button {
|
||||
case "left":
|
||||
buttons = 1
|
||||
case "right":
|
||||
buttons = 2
|
||||
case "middle":
|
||||
buttons = 4
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown button: %s", req.Button)})
|
||||
return
|
||||
}
|
||||
clickCount := 1
|
||||
if req.Double {
|
||||
clickCount = 2
|
||||
}
|
||||
for i := 0; i < clickCount; i++ {
|
||||
_, err := callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": 0, "y": 0, "buttons": buttons,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": 0, "y": 0, "buttons": 0,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "button": req.Button, "clicks": clickCount})
|
||||
}
|
||||
|
||||
func handleAPIMouseScroll(c *gin.Context) {
|
||||
var req struct {
|
||||
Delta int8 `json:"delta" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["wheelReport"], map[string]interface{}{
|
||||
"wheelY": req.Delta,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "delta": req.Delta})
|
||||
}
|
||||
|
||||
// === API Handlers: Keyboard ===
|
||||
|
||||
func handleAPIKeyboardKey(c *gin.Context) {
|
||||
var req struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
keyCode, ok := keyNameToCode[req.Key]
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown key: %s", req.Key)})
|
||||
return
|
||||
}
|
||||
action := strings.ToLower(req.Action)
|
||||
if action == "" {
|
||||
action = "press"
|
||||
}
|
||||
switch action {
|
||||
case "press":
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
case "down":
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
case "up":
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown action: %s", req.Action)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "key": req.Key, "action": action})
|
||||
}
|
||||
|
||||
func handleAPIKeyboardCombo(c *gin.Context) {
|
||||
var req struct {
|
||||
Keys []string `json:"keys" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var keys []uint8
|
||||
var modifier uint8
|
||||
for _, keyName := range req.Keys {
|
||||
switch strings.ToLower(keyName) {
|
||||
case "ctrl", "control":
|
||||
modifier |= 0x01
|
||||
continue
|
||||
case "shift":
|
||||
modifier |= 0x02
|
||||
continue
|
||||
case "alt":
|
||||
modifier |= 0x04
|
||||
continue
|
||||
case "meta", "win", "cmd":
|
||||
modifier |= 0x08
|
||||
continue
|
||||
}
|
||||
keyCode, ok := keyNameToCode[keyName]
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown key: %s", keyName)})
|
||||
return
|
||||
}
|
||||
keys = append(keys, keyCode)
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": modifier, "keys": keys,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "keys": req.Keys})
|
||||
}
|
||||
|
||||
func handleAPIKeyboardType(c *gin.Context) {
|
||||
var req struct {
|
||||
Text string `json:"text" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
for _, char := range req.Text {
|
||||
keyCode, modifier, ok := charToKeyCode(uint8(char))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": modifier, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "text": req.Text})
|
||||
}
|
||||
|
||||
// === API Handlers: Capture ===
|
||||
|
||||
func handleAPICapture(c *gin.Context) {
|
||||
data, err := captureScreenshot("jpeg")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "image/jpeg", data)
|
||||
}
|
||||
|
||||
// === API Handlers: Status ===
|
||||
|
||||
func handleAPIStatus(c *gin.Context) {
|
||||
state := lastVideoState
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"video": gin.H{
|
||||
"ready": state.Ready,
|
||||
"width": state.Width,
|
||||
"height": state.Height,
|
||||
"fps": state.FramePerSecond,
|
||||
"error": state.Error,
|
||||
},
|
||||
"device": gin.H{
|
||||
"name": "PicoKVM",
|
||||
"version": builtAppVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func handleAPIVideoState(c *gin.Context) {
|
||||
state := lastVideoState
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ready": state.Ready,
|
||||
"width": state.Width,
|
||||
"height": state.Height,
|
||||
"fps": state.FramePerSecond,
|
||||
"error": state.Error,
|
||||
})
|
||||
}
|
||||
541
cli.go
Normal file
541
cli.go
Normal file
@@ -0,0 +1,541 @@
|
||||
package kvm
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cliRootCmd = &cobra.Command{
|
||||
Use: "kvm_app cli",
|
||||
Short: "PicoKVM CLI tools",
|
||||
Long: `Command line interface for controlling HID devices via API`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
func RunCLI(args []string) {
|
||||
cliRootCmd.SetArgs(args)
|
||||
if err := cliRootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var apiBaseURL = "http://localhost:8080/api/lan"
|
||||
|
||||
func apiPost(path string, body interface{}) error {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Post(apiBaseURL+path, "application/json", strings.NewReader(string(jsonBody)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to API server: %w\nIs kvm_app running?", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
var errResp map[string]interface{}
|
||||
if json.Unmarshal(bodyBytes, &errResp) == nil {
|
||||
if msg, ok := errResp["error"].(string); ok {
|
||||
return fmt.Errorf("API error: %s", msg)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("API error: HTTP %d - %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiGetBytes(path string) ([]byte, error) {
|
||||
resp, err := http.Get(apiBaseURL + path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to API server: %w\nIs kvm_app running?", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API error: HTTP %d - %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func apiGetJSON(path string) (map[string]interface{}, error) {
|
||||
resp, err := http.Get(apiBaseURL + path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to API server: %w\nIs kvm_app running?", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API error: HTTP %d - %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ShowHelp() {
|
||||
fmt.Println(`PicoKVM - Remote KVM over IP
|
||||
|
||||
Usage:
|
||||
kvm_app Start the main web service (default)
|
||||
kvm_app cli <command> Run CLI commands for HID control
|
||||
|
||||
Commands:
|
||||
cli HID control via API (mouse, keyboard, capture, status, signer)
|
||||
|
||||
Examples:
|
||||
kvm_app cli mouse move 100 200
|
||||
kvm_app cli mouse click left
|
||||
kvm_app cli keyboard type "Hello World"
|
||||
kvm_app cli capture
|
||||
kvm_app cli status
|
||||
kvm_app cli signer keygen
|
||||
kvm_app cli signer sign --key ota_ed25519.key firmware.bin
|
||||
kvm_app cli signer verify --pubkey ota_ed25519.pub firmware.bin
|
||||
|
||||
For more help on a specific command, use:
|
||||
kvm_app cli --help`)
|
||||
}
|
||||
|
||||
// === Mouse Commands ===
|
||||
|
||||
var mouseCmd = &cobra.Command{
|
||||
Use: "mouse",
|
||||
Short: "Mouse control commands",
|
||||
}
|
||||
|
||||
var mouseMoveCmd = &cobra.Command{
|
||||
Use: "move [x] [y]",
|
||||
Short: "Move mouse to position",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
x, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid x coordinate: %w", err)
|
||||
}
|
||||
y, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid y coordinate: %w", err)
|
||||
}
|
||||
|
||||
relative, _ := cmd.Flags().GetBool("relative")
|
||||
if relative {
|
||||
return apiPost("/mouse/relative", map[string]interface{}{
|
||||
"dx": x, "dy": y,
|
||||
})
|
||||
}
|
||||
return apiPost("/mouse/absolute", map[string]interface{}{
|
||||
"x": x, "y": y,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var mouseClickCmd = &cobra.Command{
|
||||
Use: "click [button]",
|
||||
Short: "Click a mouse button",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return apiPost("/mouse/click", map[string]interface{}{
|
||||
"button": args[0],
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var mouseScrollCmd = &cobra.Command{
|
||||
Use: "scroll [delta]",
|
||||
Short: "Scroll mouse wheel",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
delta, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid delta: %w", err)
|
||||
}
|
||||
return apiPost("/mouse/scroll", map[string]interface{}{
|
||||
"delta": delta,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var mouseDownCmd = &cobra.Command{
|
||||
Use: "down [button]",
|
||||
Short: "Press and hold a mouse button",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// API doesn't support hold yet, simulate with click
|
||||
return apiPost("/mouse/click", map[string]interface{}{
|
||||
"button": args[0],
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var mouseUpCmd = &cobra.Command{
|
||||
Use: "up",
|
||||
Short: "Release all mouse buttons",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// API doesn't support hold yet, no-op
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// === Keyboard Commands ===
|
||||
|
||||
var keyboardCmd = &cobra.Command{
|
||||
Use: "keyboard",
|
||||
Short: "Keyboard control commands",
|
||||
}
|
||||
|
||||
var keyboardKeyCmd = &cobra.Command{
|
||||
Use: "key [keyname]",
|
||||
Short: "Press a key",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return apiPost("/keyboard/key", map[string]interface{}{
|
||||
"key": args[0],
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var keyboardComboCmd = &cobra.Command{
|
||||
Use: "combo [keys...]",
|
||||
Short: "Press a key combination",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return apiPost("/keyboard/combo", map[string]interface{}{
|
||||
"keys": args,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var keyboardTypeCmd = &cobra.Command{
|
||||
Use: "type [text]",
|
||||
Short: "Type a text string",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return apiPost("/keyboard/type", map[string]interface{}{
|
||||
"text": args[0],
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// === Capture Command ===
|
||||
|
||||
var captureScreenshotPath = "/userdata/picokvm/screenshot/kvm_screenshot.jpg"
|
||||
|
||||
var captureCmd = &cobra.Command{
|
||||
Use: "capture",
|
||||
Short: "Capture a JPEG screenshot",
|
||||
Long: "Capture screenshot using hardware JPEG encoder and save to fixed path",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
data, err := apiGetBytes("/capture")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(captureScreenshotPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write: %w", err)
|
||||
}
|
||||
fmt.Printf("Screenshot saved to %s (%d bytes)\n", captureScreenshotPath, len(data))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// === Status Command ===
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show device status",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiGetJSON("/video/state")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Video State:\n")
|
||||
fmt.Printf(" Ready: %v\n", resp["ready"])
|
||||
fmt.Printf(" Error: %s\n", resp["error"])
|
||||
fmt.Printf(" Size: %dx%d\n", int(resp["width"].(float64)), int(resp["height"].(float64)))
|
||||
fmt.Printf(" FPS: %.1f\n", resp["fps"])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// === Key Maps ===
|
||||
|
||||
var keyNameToCode = map[string]uint8{
|
||||
"a": 0x04, "b": 0x05, "c": 0x06, "d": 0x07,
|
||||
"e": 0x08, "f": 0x09, "g": 0x0A, "h": 0x0B,
|
||||
"i": 0x0C, "j": 0x0D, "k": 0x0E, "l": 0x0F,
|
||||
"m": 0x10, "n": 0x11, "o": 0x12, "p": 0x13,
|
||||
"q": 0x14, "r": 0x15, "s": 0x16, "t": 0x17,
|
||||
"u": 0x18, "v": 0x19, "w": 0x1A, "x": 0x1B,
|
||||
"y": 0x1C, "z": 0x1D,
|
||||
"1": 0x1E, "2": 0x1F, "3": 0x20, "4": 0x21,
|
||||
"5": 0x22, "6": 0x23, "7": 0x24, "8": 0x25,
|
||||
"9": 0x26, "0": 0x27,
|
||||
"Enter": 0x28,
|
||||
"Escape": 0x29,
|
||||
"Backspace": 0x2A,
|
||||
"Tab": 0x2B,
|
||||
"Space": 0x2C,
|
||||
"Minus": 0x2D,
|
||||
"Equal": 0x2E,
|
||||
"LeftBrace": 0x2F,
|
||||
"RightBrace": 0x30,
|
||||
"Backslash": 0x31,
|
||||
"Semicolon": 0x33,
|
||||
"Quote": 0x34,
|
||||
"Grave": 0x35,
|
||||
"Comma": 0x36,
|
||||
"Dot": 0x37,
|
||||
"Slash": 0x38,
|
||||
"CapsLock": 0x39,
|
||||
"F1": 0x3A, "F2": 0x3B, "F3": 0x3C, "F4": 0x3D,
|
||||
"F5": 0x3E, "F6": 0x3F, "F7": 0x40, "F8": 0x41,
|
||||
"F9": 0x42, "F10": 0x43, "F11": 0x44, "F12": 0x45,
|
||||
"PrintScreen": 0x46,
|
||||
"ScrollLock": 0x47,
|
||||
"Pause": 0x48,
|
||||
"Insert": 0x49,
|
||||
"Home": 0x4A,
|
||||
"PageUp": 0x4B,
|
||||
"Delete": 0x4C,
|
||||
"End": 0x4D,
|
||||
"PageDown": 0x4E,
|
||||
"Right": 0x4F,
|
||||
"Left": 0x50,
|
||||
"Down": 0x51,
|
||||
"Up": 0x52,
|
||||
}
|
||||
|
||||
func charToKeyCode(char uint8) (keyCode uint8, modifier uint8, ok bool) {
|
||||
if char >= 'a' && char <= 'z' {
|
||||
return keyNameToCode[string(char)], 0, true
|
||||
}
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
return keyNameToCode[strings.ToLower(string(char))], 0x02, true
|
||||
}
|
||||
if char >= '0' && char <= '9' {
|
||||
return keyNameToCode[string(char)], 0, true
|
||||
}
|
||||
if char == ' ' {
|
||||
return 0x2C, 0, true
|
||||
}
|
||||
switch char {
|
||||
case '-':
|
||||
return 0x2D, 0, true
|
||||
case '=':
|
||||
return 0x2E, 0, true
|
||||
case '[':
|
||||
return 0x2F, 0, true
|
||||
case ']':
|
||||
return 0x30, 0, true
|
||||
case '\\':
|
||||
return 0x31, 0, true
|
||||
case ';':
|
||||
return 0x33, 0, true
|
||||
case '\'':
|
||||
return 0x34, 0, true
|
||||
case '`':
|
||||
return 0x35, 0, true
|
||||
case ',':
|
||||
return 0x36, 0, true
|
||||
case '.':
|
||||
return 0x37, 0, true
|
||||
case '/':
|
||||
return 0x38, 0, true
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
var signerCmd = &cobra.Command{
|
||||
Use: "signer",
|
||||
Short: "OTA firmware signing tool",
|
||||
Long: "Generate keys, sign, and verify OTA firmware using Ed25519",
|
||||
}
|
||||
|
||||
var signerKeygenCmd = &cobra.Command{
|
||||
Use: "keygen",
|
||||
Short: "Generate Ed25519 key pair",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
outputDir, _ := cmd.Flags().GetString("output-dir")
|
||||
|
||||
pubKeyPath := filepath.Join(outputDir, "ota_ed25519.pub")
|
||||
privKeyPath := filepath.Join(outputDir, "ota_ed25519.key")
|
||||
|
||||
if _, err := os.Stat(privKeyPath); err == nil {
|
||||
return fmt.Errorf("private key already exists at %s, refusing to overwrite", privKeyPath)
|
||||
}
|
||||
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating key pair: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating output directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(privKeyPath, privateKey, 0600); err != nil {
|
||||
return fmt.Errorf("writing private key: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(pubKeyPath, publicKey, 0644); err != nil {
|
||||
return fmt.Errorf("writing public key: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(hex.EncodeToString(publicKey))
|
||||
fmt.Fprintf(os.Stderr, "Private key: %s\n", privKeyPath)
|
||||
fmt.Fprintf(os.Stderr, "Public key: %s\n", pubKeyPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var signerSignCmd = &cobra.Command{
|
||||
Use: "sign --key <private-key-path> <firmware-file>",
|
||||
Short: "Sign a firmware file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyPath, _ := cmd.Flags().GetString("key")
|
||||
filePath := args[0]
|
||||
|
||||
if keyPath == "" {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
|
||||
privateKey, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading private key: %w", err)
|
||||
}
|
||||
|
||||
if len(privateKey) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("invalid private key size: got %d bytes, expected %d", len(privateKey), ed25519.PrivateKeySize)
|
||||
}
|
||||
|
||||
fileData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading firmware file: %w", err)
|
||||
}
|
||||
|
||||
signature := ed25519.Sign(ed25519.PrivateKey(privateKey), fileData)
|
||||
|
||||
sigPath := filePath + ".sig"
|
||||
if err := os.WriteFile(sigPath, signature, 0644); err != nil {
|
||||
return fmt.Errorf("writing signature: %w", err)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(fileData)
|
||||
fmt.Println(hex.EncodeToString(hash[:]))
|
||||
fmt.Fprintf(os.Stderr, "Signature written to: %s\n", sigPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var signerVerifyCmd = &cobra.Command{
|
||||
Use: "verify --pubkey <pubkey-path-or-hex> <firmware-file> [<sig-file>]",
|
||||
Short: "Verify firmware signature",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
pubKeyArg, _ := cmd.Flags().GetString("pubkey")
|
||||
filePath := args[0]
|
||||
sigPath := ""
|
||||
if len(args) > 1 {
|
||||
sigPath = args[1]
|
||||
}
|
||||
|
||||
if pubKeyArg == "" {
|
||||
return fmt.Errorf("--pubkey is required")
|
||||
}
|
||||
|
||||
if sigPath == "" {
|
||||
sigPath = filePath + ".sig"
|
||||
}
|
||||
|
||||
var publicKey ed25519.PublicKey
|
||||
if _, err := os.Stat(pubKeyArg); err == nil {
|
||||
keyBytes, err := os.ReadFile(pubKeyArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading public key file: %w", err)
|
||||
}
|
||||
publicKey = ed25519.PublicKey(keyBytes)
|
||||
} else {
|
||||
keyBytes, err := hex.DecodeString(pubKeyArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding public key hex: %w", err)
|
||||
}
|
||||
publicKey = ed25519.PublicKey(keyBytes)
|
||||
}
|
||||
|
||||
if len(publicKey) != ed25519.PublicKeySize {
|
||||
return fmt.Errorf("invalid public key size: got %d bytes, expected %d", len(publicKey), ed25519.PublicKeySize)
|
||||
}
|
||||
|
||||
fileData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading firmware file: %w", err)
|
||||
}
|
||||
|
||||
sigBytes, err := os.ReadFile(sigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading signature file: %w", err)
|
||||
}
|
||||
|
||||
if len(sigBytes) != ed25519.SignatureSize {
|
||||
return fmt.Errorf("invalid signature size: got %d bytes, expected %d", len(sigBytes), ed25519.SignatureSize)
|
||||
}
|
||||
|
||||
if !ed25519.Verify(publicKey, fileData, sigBytes) {
|
||||
return fmt.Errorf("VERIFICATION FAILED: signature is invalid")
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(fileData)
|
||||
fmt.Fprintf(os.Stderr, "VERIFICATION OK: signature is valid\n")
|
||||
fmt.Fprintf(os.Stderr, "SHA256: %s\n", hex.EncodeToString(hash[:]))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
mouseMoveCmd.Flags().Bool("relative", false, "Use relative positioning")
|
||||
mouseCmd.AddCommand(mouseMoveCmd)
|
||||
mouseCmd.AddCommand(mouseClickCmd)
|
||||
mouseCmd.AddCommand(mouseScrollCmd)
|
||||
mouseCmd.AddCommand(mouseDownCmd)
|
||||
mouseCmd.AddCommand(mouseUpCmd)
|
||||
cliRootCmd.AddCommand(mouseCmd)
|
||||
|
||||
keyboardCmd.AddCommand(keyboardKeyCmd)
|
||||
keyboardCmd.AddCommand(keyboardComboCmd)
|
||||
keyboardCmd.AddCommand(keyboardTypeCmd)
|
||||
cliRootCmd.AddCommand(keyboardCmd)
|
||||
|
||||
cliRootCmd.AddCommand(captureCmd)
|
||||
|
||||
signerKeygenCmd.Flags().String("output-dir", ".", "Directory for key output")
|
||||
signerCmd.AddCommand(signerKeygenCmd)
|
||||
signerSignCmd.Flags().String("key", "", "Path to private key")
|
||||
signerCmd.AddCommand(signerSignCmd)
|
||||
signerVerifyCmd.Flags().String("pubkey", "", "Path to public key file or hex-encoded public key")
|
||||
signerCmd.AddCommand(signerVerifyCmd)
|
||||
cliRootCmd.AddCommand(signerCmd)
|
||||
|
||||
cliRootCmd.AddCommand(statusCmd)
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"kvm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "cli" {
|
||||
kvm.RunCLI(os.Args[2:])
|
||||
return
|
||||
}
|
||||
kvm.Main()
|
||||
}
|
||||
|
||||
25
go.mod
25
go.mod
@@ -1,14 +1,11 @@
|
||||
module kvm
|
||||
|
||||
go 1.23.4
|
||||
|
||||
toolchain go1.24.3
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/creack/pty v1.1.23
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
@@ -17,8 +14,10 @@ require (
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||
github.com/mark3labs/mcp-go v0.52.0
|
||||
github.com/pion/logging v0.2.3
|
||||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/rtp v1.8.18
|
||||
github.com/pion/webrtc/v4 v4.0.16
|
||||
github.com/pojntfx/go-nbd v0.3.2
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
@@ -27,6 +26,7 @@ require (
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
go.bug.st/serial v1.6.2
|
||||
@@ -47,11 +47,12 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -63,35 +64,29 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pilebones/go-udev v0.9.0 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v2 v2.3.36 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.18 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.20 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pion/webrtc/v3 v3.3.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/arch v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
109
go.sum
109
go.sum
@@ -18,9 +18,8 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
@@ -28,6 +27,10 @@ github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
@@ -38,8 +41,6 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -54,7 +55,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||
@@ -63,6 +65,8 @@ github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
@@ -71,7 +75,6 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -83,6 +86,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mark3labs/mcp-go v0.52.0 h1:uRSzupNSUyPGDpF4owY5X4zEpACPwBnlM3FAFuXN6gQ=
|
||||
github.com/mark3labs/mcp-go v0.52.0/go.mod h1:Zg9cB2HdwdMMVgY0xtTzq3KvYIOJQDsaut+jWjwDaQY=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -105,59 +110,34 @@ github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl
|
||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc=
|
||||
github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
|
||||
github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
|
||||
github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA=
|
||||
github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -178,8 +158,17 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -188,8 +177,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -200,81 +187,31 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
||||
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
55
jsonrpc.go
55
jsonrpc.go
@@ -1496,6 +1496,61 @@ func rpcConfirmOtherSession() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
const jpegScreenshotPath = "/userdata/picokvm/screenshot/kvm_screenshot.jpg"
|
||||
|
||||
// StartJpegCapture starts continuous JPEG capture mode.
|
||||
func StartJpegCapture() error {
|
||||
_, err := CallCtrlAction("jpeg_capture_start", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start JPEG capture: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopJpegCapture stops continuous JPEG capture mode.
|
||||
func StopJpegCapture() error {
|
||||
_, err := CallCtrlAction("jpeg_capture_stop", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop JPEG capture: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// captureScreenshot captures a JPEG screenshot using hardware encoder.
|
||||
func captureScreenshot(format string) ([]byte, error) {
|
||||
if format != "jpeg" && format != "jpg" {
|
||||
return nil, fmt.Errorf("only JPEG format is supported")
|
||||
}
|
||||
|
||||
logger.Info().Msg("triggering JPEG snapshot via jpeg_take_snapshot")
|
||||
|
||||
if err := os.MkdirAll("/userdata/picokvm/screenshot", 0o755); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to create screenshot directory")
|
||||
}
|
||||
|
||||
os.Remove(jpegScreenshotPath)
|
||||
|
||||
resp, err := CallCtrlAction("jpeg_take_snapshot", nil)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("jpeg_take_snapshot failed")
|
||||
return nil, fmt.Errorf("failed to trigger JPEG capture: %w", err)
|
||||
}
|
||||
logger.Info().Interface("response", resp).Msg("jpeg_take_snapshot response")
|
||||
|
||||
// Poll for file with timeout
|
||||
maxAttempts := 10
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
if data, err := os.ReadFile(jpegScreenshotPath); err == nil && len(data) > 0 {
|
||||
logger.Info().Int("size", len(data)).Int("attempts", i+1).Msg("JPEG captured successfully")
|
||||
return data, nil
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
logger.Error().Str("path", jpegScreenshotPath).Msg("JPEG file not found after timeout")
|
||||
return nil, fmt.Errorf("JPEG file not found at %s", jpegScreenshotPath)
|
||||
}
|
||||
|
||||
var rpcHandlers = map[string]RPCHandler{
|
||||
"ping": {Func: rpcPing},
|
||||
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
||||
|
||||
9
main.go
9
main.go
@@ -185,6 +185,15 @@ func Main() {
|
||||
//go RunFuseServer()
|
||||
go RunWebServer()
|
||||
|
||||
// API and MCP services temporarily disabled for debugging
|
||||
go func() {
|
||||
StartAPIServer(8080)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
StartMCP(8081, false)
|
||||
}()
|
||||
|
||||
go RunWebSecureServer()
|
||||
// Web secure server is started only if TLS mode is enabled
|
||||
if config.TLSMode != "" {
|
||||
|
||||
310
mcp.go
Normal file
310
mcp.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package kvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func StartMCP(port int, stdio bool) {
|
||||
s := server.NewMCPServer("picokvm-mcp", "1.0.0")
|
||||
registerMCPTools(s)
|
||||
|
||||
if stdio {
|
||||
logger.Info().Msg("Starting MCP stdio server")
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
logger.Error().Err(err).Msg("MCP stdio server failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SSE mode
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
sseServer := server.NewSSEServer(s)
|
||||
handler := sseServer.SSEHandler()
|
||||
|
||||
// Add auth for non-localhost
|
||||
if config.APIKey != "" {
|
||||
handler = withAPIKeyAuth(handler, config.APIKey)
|
||||
}
|
||||
handler = withCORS(handler)
|
||||
|
||||
logger.Info().Str("addr", addr).Msg("Starting MCP SSE server")
|
||||
if err := http.ListenAndServe(addr, handler); err != nil {
|
||||
logger.Error().Err(err).Msg("MCP SSE server failed")
|
||||
}
|
||||
}
|
||||
|
||||
// === Shared middleware helpers ===
|
||||
|
||||
func withCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func withAPIKeyAuth(next http.Handler, expectedKey string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip auth for localhost
|
||||
if strings.HasPrefix(r.RemoteAddr, "127.0.0.1:") ||
|
||||
strings.HasPrefix(r.RemoteAddr, "[::1]:") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
var key string
|
||||
if _, err := fmt.Sscanf(auth, "Bearer %s", &key); err != nil {
|
||||
http.Error(w, `{"error":"missing or invalid authorization"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(key, expectedKey) {
|
||||
http.Error(w, `{"error":"invalid api key"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// === MCP Tool Registration ===
|
||||
|
||||
func registerMCPTools(s *server.MCPServer) {
|
||||
s.AddTool(mcp.NewTool("mouse_move_absolute",
|
||||
mcp.WithDescription("Move mouse to absolute coordinates (0-32767)"),
|
||||
mcp.WithNumber("x", mcp.Required(), mcp.Description("X coordinate")),
|
||||
mcp.WithNumber("y", mcp.Required(), mcp.Description("Y coordinate")),
|
||||
), handleMouseMoveAbsolute)
|
||||
|
||||
s.AddTool(mcp.NewTool("mouse_move_relative",
|
||||
mcp.WithDescription("Move mouse by relative offset"),
|
||||
mcp.WithNumber("dx", mcp.Required()),
|
||||
mcp.WithNumber("dy", mcp.Required()),
|
||||
), handleMouseMoveRelative)
|
||||
|
||||
s.AddTool(mcp.NewTool("mouse_click",
|
||||
mcp.WithDescription("Click mouse button"),
|
||||
mcp.WithString("button", mcp.Required(), mcp.Enum("left", "right", "middle")),
|
||||
), handleMouseClick)
|
||||
|
||||
s.AddTool(mcp.NewTool("mouse_scroll",
|
||||
mcp.WithDescription("Scroll mouse wheel"),
|
||||
mcp.WithNumber("delta", mcp.Required()),
|
||||
), handleMouseScroll)
|
||||
|
||||
s.AddTool(mcp.NewTool("keyboard_key",
|
||||
mcp.WithDescription("Press a key"),
|
||||
mcp.WithString("key", mcp.Required(), mcp.Description("Key name: Enter, Escape, Tab, etc.")),
|
||||
), handleKeyboardKey)
|
||||
|
||||
s.AddTool(mcp.NewTool("keyboard_combo",
|
||||
mcp.WithDescription("Press key combination"),
|
||||
mcp.WithArray("keys", mcp.Required(), mcp.Items(map[string]any{"type": "string"})),
|
||||
), handleKeyboardCombo)
|
||||
|
||||
s.AddTool(mcp.NewTool("type_text",
|
||||
mcp.WithDescription("Type text string"),
|
||||
mcp.WithString("text", mcp.Required()),
|
||||
), handleTypeText)
|
||||
|
||||
s.AddTool(mcp.NewTool("capture_screenshot",
|
||||
mcp.WithDescription("Capture JPEG screenshot using hardware encoder"),
|
||||
), handleCaptureScreenshot)
|
||||
|
||||
s.AddTool(mcp.NewTool("get_video_state",
|
||||
mcp.WithDescription("Get screen resolution and video status"),
|
||||
), handleGetVideoState)
|
||||
}
|
||||
|
||||
// === MCP Handlers ===
|
||||
|
||||
func handleMouseMoveAbsolute(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
x, _ := args["x"].(float64)
|
||||
y, _ := args["y"].(float64)
|
||||
_, err := callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": x, "y": y, "buttons": 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Mouse moved to (%d, %d)", int(x), int(y))), nil
|
||||
}
|
||||
|
||||
func handleMouseMoveRelative(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
dx, _ := args["dx"].(float64)
|
||||
dy, _ := args["dy"].(float64)
|
||||
_, err := callRPCHandler(rpcHandlers["relMouseReport"], map[string]interface{}{
|
||||
"dx": dx, "dy": dy, "buttons": 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Mouse moved by (%d, %d)", int(dx), int(dy))), nil
|
||||
}
|
||||
|
||||
func handleMouseClick(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
button, _ := args["button"].(string)
|
||||
var buttons uint8
|
||||
switch button {
|
||||
case "left":
|
||||
buttons = 1
|
||||
case "right":
|
||||
buttons = 2
|
||||
case "middle":
|
||||
buttons = 4
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": 0, "y": 0, "buttons": buttons,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": 0, "y": 0, "buttons": 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Clicked %s button", button)), nil
|
||||
}
|
||||
|
||||
func handleMouseScroll(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
delta, _ := args["delta"].(float64)
|
||||
_, err := callRPCHandler(rpcHandlers["wheelReport"], map[string]interface{}{
|
||||
"wheelY": delta,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Scrolled by %d", int(delta))), nil
|
||||
}
|
||||
|
||||
func handleKeyboardKey(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
keyName, _ := args["key"].(string)
|
||||
keyCode, ok := keyNameToCode[keyName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown key: %s", keyName)
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Pressed key: %s", keyName)), nil
|
||||
}
|
||||
|
||||
func handleKeyboardCombo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
keysArg, _ := args["keys"].([]interface{})
|
||||
var keys []uint8
|
||||
var modifier uint8
|
||||
|
||||
for _, k := range keysArg {
|
||||
keyName, _ := k.(string)
|
||||
switch strings.ToLower(keyName) {
|
||||
case "ctrl", "control":
|
||||
modifier |= 0x01
|
||||
continue
|
||||
case "shift":
|
||||
modifier |= 0x02
|
||||
continue
|
||||
case "alt":
|
||||
modifier |= 0x04
|
||||
continue
|
||||
case "meta", "win", "cmd":
|
||||
modifier |= 0x08
|
||||
continue
|
||||
}
|
||||
keyCode, ok := keyNameToCode[keyName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown key: %s", keyName)
|
||||
}
|
||||
keys = append(keys, keyCode)
|
||||
}
|
||||
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": modifier, "keys": keys,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Pressed combo: %v", keysArg)), nil
|
||||
}
|
||||
|
||||
func handleTypeText(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := req.GetArguments()
|
||||
text, _ := args["text"].(string)
|
||||
for _, char := range text {
|
||||
keyCode, modifier, ok := charToKeyCode(uint8(char))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": modifier, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Typed: %s", text)), nil
|
||||
}
|
||||
|
||||
func handleCaptureScreenshot(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := captureScreenshot("jpeg")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base64Data := base64.StdEncoding.EncodeToString(data)
|
||||
return mcp.NewToolResultImage("JPEG screenshot captured", base64Data, "image/jpeg"), nil
|
||||
}
|
||||
|
||||
func handleGetVideoState(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
result, err := callRPCHandler(rpcHandlers["getVideoState"], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state, ok := result.(VideoInputState)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected video state type")
|
||||
}
|
||||
text := fmt.Sprintf("Video: %dx%d @ %.1f fps (Ready: %v)", state.Width, state.Height, state.FramePerSecond, state.Ready)
|
||||
if state.Error != "" {
|
||||
text += fmt.Sprintf(" [Error: %s]", state.Error)
|
||||
}
|
||||
return mcp.NewToolResultText(text), nil
|
||||
}
|
||||
2
video.go
2
video.go
@@ -24,7 +24,7 @@ type VideoInputState struct {
|
||||
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
FramePerSecond float64 `json:"fps"`
|
||||
FramePerSecond float64 `json:"frame_per_second"`
|
||||
}
|
||||
|
||||
var lastVideoState VideoInputState
|
||||
|
||||
Reference in New Issue
Block a user