diff --git a/cmd/waiter/builtin.go b/cmd/waiter/builtin.go index d091d82..94bae95 100644 --- a/cmd/waiter/builtin.go +++ b/cmd/waiter/builtin.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "strings" ) @@ -28,6 +29,7 @@ Server commands (sent to agent): /plugin info plugin details /memory query query graph memory /knowledge list knowledge base + /knowledge delete delete knowledge item /agents list agents /chat send to agent @@ -64,7 +66,136 @@ Any other text is sent to the agent directly.`) reconnect() return true + case cmd == "/status": + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/status", "") + printJSON(d) + } else { + state.Send("/status") + } + return true + + case cmd == "/kernel": + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/kernel", "") + printJSON(d) + } else { + state.Send("/kernel") + } + return true + + case strings.HasPrefix(cmd, "/settings set "): + parts := strings.SplitN(cmd[14:], " ", 2) + if len(parts) < 2 { + fmt.Println("usage: /settings set ") + return true + } + if rc := state.RemoteConn(); rc != nil { + body := fmt.Sprintf(`{"%s":%q}`, parts[0], parts[1]) + rc.DoAPI("PUT", "/api/v1/settings", body) + fmt.Println("ok") + } else { + state.Send(cmd[1:]) + } + return true + + case strings.HasPrefix(cmd, "/settings"): + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/settings", "") + printJSON(d) + } else { + state.Send(cmd[1:]) + } + return true + + case cmd == "/plugin list": + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/plugins", "") + printJSON(d) + } else { + state.Send("/plugin list") + } + return true + + case strings.HasPrefix(cmd, "/plugin install "): + url := strings.TrimSpace(cmd[16:]) + if rc := state.RemoteConn(); rc != nil { + body := fmt.Sprintf(`{"url":%q}`, url) + d, _ := rc.DoAPI("POST", "/api/v1/plugins", body) + printJSON(d) + } else { + state.Send(cmd[1:]) + } + return true + + case strings.HasPrefix(cmd, "/plugin remove "): + name := strings.TrimSpace(cmd[15:]) + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("DELETE", "/api/v1/plugins/"+name, "") + printJSON(d) + } else { + state.Send(cmd[1:]) + } + return true + + case strings.HasPrefix(cmd, "/plugin info "): + name := strings.TrimSpace(cmd[13:]) + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/plugins/"+name, "") + printJSON(d) + } else { + state.Send(cmd[1:]) + } + return true + + case strings.HasPrefix(cmd, "/memory query "): + q := strings.TrimSpace(cmd[14:]) + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/memory?query="+q, "") + printJSON(d) + } else { + state.Send(cmd[1:]) + } + return true + + case strings.HasPrefix(cmd, "/knowledge delete "): + name := strings.TrimSpace(cmd[18:]) + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("DELETE", "/api/v1/knowledge/"+name, "") + printJSON(d) + } else { + state.Send(cmd[1:]) + } + return true + + case cmd == "/knowledge": + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/knowledge", "") + printJSON(d) + } else { + state.Send("/knowledge") + } + return true + + case cmd == "/agents": + if rc := state.RemoteConn(); rc != nil { + d, _ := rc.DoAPI("GET", "/api/v1/agents", "") + printJSON(d) + } else { + state.Send("/agents") + } + return true + default: return false } } + +func printJSON(d map[string]interface{}) { + if d == nil { + fmt.Println("(no data)") + return + } + b, _ := json.MarshalIndent(d, "", " ") + fmt.Println(string(b)) +} diff --git a/cmd/waiter/conn.go b/cmd/waiter/conn.go index 61bc1d6..f043ad7 100644 --- a/cmd/waiter/conn.go +++ b/cmd/waiter/conn.go @@ -76,19 +76,19 @@ func (c *localConn) Close() error { func dialRemote(baseURL, apiKey string) (Conn, error) { baseURL = strings.TrimRight(baseURL, "/") - return &remoteConn{url: baseURL + "/api/v1/chat", apiKey: apiKey}, nil + return &remoteConn{baseURL: baseURL, apiKey: apiKey}, nil } type remoteConn struct { - url string - apiKey string - mu sync.Mutex - buf []string - closed bool + baseURL string + apiKey string + mu sync.Mutex + buf []string + closed bool } func (c *remoteConn) Send(line string) error { - req, err := http.NewRequest("POST", c.url, strings.NewReader( + req, err := http.NewRequest("POST", c.baseURL+"/api/v1/chat", strings.NewReader( fmt.Sprintf(`{"message":%q}`, line), )) if err != nil { @@ -117,6 +117,31 @@ func (c *remoteConn) Send(line string) error { return nil } +func (c *remoteConn) DoAPI(method, path string, body string) (map[string]interface{}, error) { + var bodyReader io.Reader + if body != "" { + bodyReader = strings.NewReader(body) + } + req, err := http.NewRequest(method, c.baseURL+path, bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + if c.apiKey != "" { + req.Header.Set("X-API-Key", c.apiKey) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + func (c *remoteConn) ReadLine() (string, error) { c.mu.Lock() defer c.mu.Unlock() diff --git a/cmd/waiter/state.go b/cmd/waiter/state.go index c3a8671..e75be56 100644 --- a/cmd/waiter/state.go +++ b/cmd/waiter/state.go @@ -41,6 +41,15 @@ func (s *State) Connected() bool { return s.conn != nil } +func (s *State) RemoteConn() *remoteConn { + s.mu.Lock() + defer s.mu.Unlock() + if rc, ok := s.conn.(*remoteConn); ok { + return rc + } + return nil +} + func (s *State) Send(line string) error { s.mu.Lock() c := s.conn