98 lines
3.1 KiB
Go
98 lines
3.1 KiB
Go
package agents
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"g.fc.run/ai/byre/internal/mcp"
|
|
)
|
|
|
|
func (m *Manager) renderDiagramTool() mcp.Tool {
|
|
return mcp.Tool{
|
|
Name: "render_diagram",
|
|
Description: "See a diagram as byre's app draws it in the transcript: pass the body of a ```diagram block and get back a PNG of the result, drawn by the app's own renderer in light mode. " +
|
|
"A source the app can't draw comes back as the reason instead.",
|
|
InputSchema: json.RawMessage(`{
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {"type": "string", "description": "The block's body, without the fence lines."}
|
|
},
|
|
"required": ["source"]
|
|
}`),
|
|
Handler: mcp.TypedResult(func(ctx context.Context, args struct {
|
|
Source string `json:"source"`
|
|
}) mcp.Result {
|
|
png, err := renderDiagram(ctx, args.Source)
|
|
if err != nil {
|
|
return mcp.TextResult(err.Error(), true)
|
|
}
|
|
return mcp.Result{Content: []mcp.Content{{Type: "image", Data: base64.StdEncoding.EncodeToString(png), MimeType: "image/png"}}}
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (m *Manager) exportDiagramTool(l *live) mcp.Tool {
|
|
return mcp.Tool{
|
|
Name: "export_diagram",
|
|
Description: "Export a diagram as a self-contained HTML page, drawn exactly as byre's app lays it out and as interactive as its full-size view: clicking a box highlights it, its arrows and its neighbors, each highlight line is a button, and it zooms and pans. " +
|
|
"Pass the body of a ```diagram block and the file to write; show it with show_page.",
|
|
InputSchema: json.RawMessage(`{
|
|
"type": "object",
|
|
"properties": {
|
|
"source": {"type": "string", "description": "The block's body, without the fence lines."},
|
|
"path": {"type": "string", "description": "The HTML file to write, relative to your worktree, absolute, or starting with ~/."}
|
|
},
|
|
"required": ["source", "path"]
|
|
}`),
|
|
Handler: mcp.Typed(func(ctx context.Context, args struct {
|
|
Source string `json:"source"`
|
|
Path string `json:"path"`
|
|
}) (string, error) {
|
|
full, err := l.resolve(args.Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
html, err := renderDiagram(ctx, args.Source, "--html")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = os.MkdirAll(filepath.Dir(full), 0o755)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = os.WriteFile(full, html, 0o644)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Wrote the diagram to %s.", args.Path), nil
|
|
}),
|
|
}
|
|
}
|
|
|
|
func renderDiagram(ctx context.Context, source string, flags ...string) ([]byte, error) {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
app := filepath.Join(filepath.Dir(exe), "..", "MacOS", "Byre")
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, app, append([]string{"render-diagram"}, flags...)...)
|
|
cmd.Stdin = strings.NewReader(source)
|
|
stderr := bytes.Buffer{}
|
|
cmd.Stderr = &stderr
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("render diagram: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
return out, nil
|
|
}
|