cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / cowork / endpoint.go
endpoint.go 4.4K
1package cowork23import (4 "encoding/json"5 "errors"6 "fmt"7 "io/fs"8 "net"9 "os"10 "path/filepath"11 "slices"12 "strings"13 "time"1415 "src.rest/cat/012/internal/config"16)1718// Where sessions listen: a folder of the user's own (0700), the runtime19// folder's 012 when the system has one ($XDG_RUNTIME_DIR), else agents20// in 012's config folder. Each listening session has a Unix socket21// there (0600), <pid>-<n>.sock, and beside it an endpoint file,22// <pid>-<n>.json, saying what it is, which 012 mcp --attach reads to23// find it. Windows has Unix sockets too, and its folder is the user's.2425// Endpoint is what a listening session says of itself.26type Endpoint struct {27 PID int `json:"pid"`28 Socket string `json:"socket"`29 // Kind is "session" for a 012 on a terminal, "serve" for 012 serve,30 // whose agents name the file they join.31 Kind string `json:"kind"`32 // Workbook is the session's workbook, as its title names it, or the33 // folder 012 serve serves.34 Workbook string `json:"workbook"`35 Started time.Time `json:"started"`36}3738// SocketDir is the folder sessions listen in.39func SocketDir() (string, error) {40 if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" && filepath.IsAbs(d) {41 return filepath.Join(d, "012"), nil42 }43 d, err := config.Dir()44 if err != nil {45 return "", err46 }47 return filepath.Join(d, "agents"), nil48}4950// ownDir makes dir the user's own, refusing one others can reach.51func ownDir(dir string) error {52 if err := os.MkdirAll(dir, 0o700); err != nil {53 return err54 }55 if err := os.Chmod(dir, 0o700); err != nil {56 return err57 }58 return checkOwner(dir)59}6061// Endpoints are the sessions listening in dir, newest first. Files62// left by sessions that are gone are removed.63func Endpoints(dir string) ([]Endpoint, error) {64 entries, err := os.ReadDir(dir)65 if errors.Is(err, fs.ErrNotExist) {66 return nil, nil67 }68 if err != nil {69 return nil, err70 }71 var out []Endpoint72 for _, e := range entries {73 if !strings.HasSuffix(e.Name(), ".json") {74 continue75 }76 path := filepath.Join(dir, e.Name())77 data, err := os.ReadFile(path)78 if err != nil {79 continue80 }81 var ep Endpoint82 if json.Unmarshal(data, &ep) != nil || !alive(ep.Socket) {83 os.Remove(path)84 os.Remove(strings.TrimSuffix(path, ".json") + ".sock")85 continue86 }87 out = append(out, ep)88 }89 slices.SortFunc(out, func(a, b Endpoint) int { return b.Started.Compare(a.Started) })90 return out, nil91}9293// alive reports whether a session answers on socket.94func alive(socket string) bool {95 c, err := net.DialTimeout("unix", socket, time.Second)96 if err != nil {97 return false98 }99 c.Close()100 return true101}102103// Pick is the endpoint target names among eps, and the file the agent104// of 012 serve joins: "" when only one session listens, a workbook's105// name, a pid, a socket's path, or with one 012 serve listening, the106// file to join.107func Pick(eps []Endpoint, target string) (Endpoint, string, error) {108 if strings.HasSuffix(target, ".sock") {109 return Endpoint{Socket: target, Kind: "session"}, "", nil110 }111 var match []Endpoint112 for _, ep := range eps {113 if target == "" || ep.Kind == "session" && (strings.EqualFold(ep.Workbook, target) || fmt.Sprint(ep.PID) == target ||114 strings.EqualFold(ep.Workbook, strings.TrimSuffix(filepath.Base(target), filepath.Ext(target)))) {115 match = append(match, ep)116 }117 }118 if len(match) == 0 && target != "" {119 for _, ep := range eps {120 if ep.Kind == "serve" {121 match = append(match, ep)122 }123 }124 if len(match) == 1 {125 return match[0], target, nil126 }127 }128 switch len(match) {129 case 1:130 if match[0].Kind == "serve" && target == "" {131 return Endpoint{}, "", fmt.Errorf("012 serve listens for agents on %s: name the file to join, 012 mcp --attach budget.012", match[0].Workbook)132 }133 return match[0], "", nil134 case 0:135 if target == "" {136 return Endpoint{}, "", errors.New("no 012 session is listening for agents: start one with 012 --listen, or File > Invite an agent")137 }138 return Endpoint{}, "", fmt.Errorf("no 012 session listening for agents has %s open%s", target, listing(eps))139 }140 return Endpoint{}, "", fmt.Errorf("several 012 sessions are listening: name one, 012 mcp --attach <name>%s", listing(match))141}142143// listing lists endpoints for an error.144func listing(eps []Endpoint) string {145 if len(eps) == 0 {146 return ""147 }148 var b strings.Builder149 b.WriteString(":")150 for _, ep := range eps {151 what := ep.Workbook152 if ep.Kind == "serve" {153 what = "012 serve of " + ep.Workbook154 }155 fmt.Fprintf(&b, "\n %s (pid %d)", what, ep.PID)156 }157 return b.String()158}