Show images in Files and add client_open to open files on a client Mac
This commit is contained in:
@@ -2,6 +2,7 @@ package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -77,6 +78,22 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
|
||||
return m.clientUpload(ctx, l, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_open",
|
||||
Description: "Show the human a file from your worktree on a client Mac: copy it into a fresh folder under ~/Library/Caches/byre/open/ there and open it with macOS's `open`, in that Mac's default app for its type (Preview for images and PDFs, the browser for HTML). " +
|
||||
"Use it when the human asks to see a file on their Mac, or for a file the Files tab can't show; it brings an app forward on their screen.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
|
||||
"path": {"type": "string", "description": "The file or directory, relative to your worktree."}
|
||||
},
|
||||
"required": ["client", "path"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.clientOpen(ctx, l, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_download",
|
||||
Description: "Copy a file or directory from a client Mac into your worktree, replacing whatever is at dest there. " +
|
||||
@@ -264,10 +281,17 @@ func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
clean, err := m.upload(ctx, l, args.Client, args.Path, args.Dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Copied %s to %s on %s.", clean, args.Dest, args.Client), nil
|
||||
}
|
||||
|
||||
clean := filepath.Clean(args.Path)
|
||||
func (m *Manager) upload(ctx context.Context, l *live, client, path, dest string) (string, error) {
|
||||
clean := filepath.Clean(path)
|
||||
if filepath.IsAbs(clean) {
|
||||
return "", fmt.Errorf("path %q must be relative to your worktree", args.Path)
|
||||
return "", fmt.Errorf("path %q must be relative to your worktree", path)
|
||||
}
|
||||
root, err := os.OpenRoot(l.dir)
|
||||
if err != nil {
|
||||
@@ -276,7 +300,7 @@ func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage
|
||||
_, err = root.Lstat(clean)
|
||||
root.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("path %q: %w", args.Path, err)
|
||||
return "", fmt.Errorf("path %q: %w", path, err)
|
||||
}
|
||||
|
||||
full := filepath.Join(l.dir, clean)
|
||||
@@ -285,11 +309,47 @@ func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, copyTimeout)
|
||||
defer cancel()
|
||||
_, err = m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Upload, Transfer: &id, Dest: &args.Dest})
|
||||
_, err = m.clients.Call(cctx, client, api.ClientRequest{Op: api.Upload, Transfer: &id, Dest: &dest})
|
||||
return clean, err
|
||||
}
|
||||
|
||||
type clientOpenArgs struct {
|
||||
Client string `json:"client"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (m *Manager) clientOpen(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
||||
args := clientOpenArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed client_open call: %w", err)
|
||||
}
|
||||
dir := "~/Library/Caches/byre/open/" + rand.Text()
|
||||
name := filepath.Base(filepath.Clean(args.Path))
|
||||
if name == "." {
|
||||
return "", errors.New("path names your worktree, not a file in it")
|
||||
}
|
||||
dest := dir + "/" + name
|
||||
clean, err := m.upload(ctx, l, args.Client, args.Path, dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Copied %s to %s on %s.", clean, args.Dest, args.Client), nil
|
||||
|
||||
command := "open " + dir + "/'" + strings.ReplaceAll(name, "'", `'\''`) + "'"
|
||||
ms := int(fileTimeout.Milliseconds())
|
||||
cctx, cancel := context.WithTimeout(ctx, fileTimeout+replyGrace)
|
||||
defer cancel()
|
||||
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Bash, Command: &command, TimeoutMs: &ms})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if reply.ExitCode == nil || reply.Output == nil {
|
||||
return "", fmt.Errorf("%s's reply has no exit_code or output", args.Client)
|
||||
}
|
||||
if *reply.ExitCode != 0 {
|
||||
return "", fmt.Errorf("copied %s to %s on %s, but open exited %d: %s", clean, dest, args.Client, *reply.ExitCode, *reply.Output)
|
||||
}
|
||||
return fmt.Sprintf("Opened %s on %s, copied to %s.", clean, args.Client, dest), nil
|
||||
}
|
||||
|
||||
func (m *Manager) clientDownload(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user