Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"keeperhub": {
"command": "kh",
"args": ["serve", "--mcp"]
}
}
}
2 changes: 1 addition & 1 deletion cmd/action/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewGetCmd(f *cmdutil.Factory) *cobra.Command {
}

query := args[0]
host := cfg.DefaultHost
host := cmdutil.ResolveHost(cmd, cfg)
apiURL := khhttp.BuildBaseURL(host) + "/api/integrations"

req, err := client.NewRequest(http.MethodGet, apiURL, nil)
Expand Down
2 changes: 1 addition & 1 deletion cmd/action/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewListCmd(f *cmdutil.Factory) *cobra.Command {
return err
}

host := cfg.DefaultHost
host := cmdutil.ResolveHost(cmd, cfg)
apiURL := khhttp.BuildBaseURL(host) + "/api/integrations"
if category != "" {
apiURL += "?category=" + category
Expand Down
2 changes: 1 addition & 1 deletion cmd/billing/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func NewStatusCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

host := cfg.DefaultHost
host := cmdutil.ResolveHost(cmd, cfg)
url := khhttp.BuildBaseURL(host) + "/api/billing/subscription"

req, err := client.NewRequest(http.MethodGet, url, nil)
Expand Down
2 changes: 1 addition & 1 deletion cmd/billing/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func NewUsageCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

host := cfg.DefaultHost
host := cmdutil.ResolveHost(cmd, cfg)
url := khhttp.BuildBaseURL(host) + "/api/billing/subscription"
if period != "" && period != "current" {
url += "?period=" + period
Expand Down
8 changes: 3 additions & 5 deletions cmd/doctor/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,14 @@ func getHTTPClient(_ *cmdutil.Factory) (*http.Client, error) {
}

// getHost returns the configured host, falling back to the default.
// NOTE: cmd is not available in check functions, so we pass nil to ResolveHost.
// The --host flag is still respected via the KH_HOST env var and config fallback.
func getHost(f *cmdutil.Factory) (string, error) {
cfg, err := f.Config()
if err != nil {
return "", err
}
host := cfg.DefaultHost
if host == "" {
host = "app.keeperhub.com"
}
return host, nil
return cmdutil.ResolveHost(nil, cfg), nil
}

// doGet performs a GET request against url with the given context, returning
Expand Down
2 changes: 1 addition & 1 deletion cmd/execute/contract_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func NewContractCallCmd(f *cmdutil.Factory) *cobra.Command {
if err != nil {
return err
}
host := cfg.DefaultHost
host := cmdutil.ResolveHost(cmd, cfg)

chain, _ := cmd.Flags().GetString("chain")
contract, _ := cmd.Flags().GetString("contract")
Expand Down
2 changes: 1 addition & 1 deletion cmd/execute/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ See also: kh r st, kh ex transfer, kh ex cc`,
if err != nil {
return err
}
host := cfg.DefaultHost
host := cmdutil.ResolveHost(cmd, cfg)

watch, _ := cmd.Flags().GetBool("watch")

Expand Down
2 changes: 1 addition & 1 deletion cmd/execute/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func NewTransferCmd(f *cmdutil.Factory) *cobra.Command {
if err != nil {
return err
}
host := cfg.DefaultHost
host := cmdutil.ResolveHost(cmd, cfg)

chain, _ := cmd.Flags().GetString("chain")
to, _ := cmd.Flags().GetString("to")
Expand Down
2 changes: 1 addition & 1 deletion cmd/org/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func NewListCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

url := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/organizations"
url := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/organizations"

req, err := client.NewRequest(http.MethodGet, url, nil)
if err != nil {
Expand Down
19 changes: 6 additions & 13 deletions cmd/org/members.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -32,8 +31,8 @@ func memberName(m Member) string {
return ""
}

// membersResponse wraps the Better Auth list-members response.
type membersResponse struct {
// listMembersResponse wraps the Better Auth list-members response.
type listMembersResponse struct {
Members []Member `json:"members"`
}

Expand All @@ -59,18 +58,12 @@ func NewMembersCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

url := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/auth/organization/list-members"
url := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/auth/organization/list-members"

bodyBytes, err := json.Marshal(map[string]string{})
if err != nil {
return fmt.Errorf("encoding request body: %w", err)
}

req, err := client.NewRequest(http.MethodPost, url, bytes.NewReader(bodyBytes))
req, err := client.NewRequest(http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("building request: %w", err)
}
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
Expand All @@ -82,9 +75,9 @@ func NewMembersCmd(f *cmdutil.Factory) *cobra.Command {
return khhttp.NewAPIError(resp)
}

var wrapper membersResponse
var wrapper listMembersResponse
if err := json.NewDecoder(resp.Body).Decode(&wrapper); err != nil {
return fmt.Errorf("unexpected response from member list endpoint: %w", err)
return fmt.Errorf("unexpected response from organization endpoint: %w", err)
}

members := wrapper.Members
Expand Down
6 changes: 3 additions & 3 deletions cmd/org/members_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func newMembersFactory(server *httptest.Server, ios *iostreams.IOStreams) *cmdut
func makeMembersServer(t *testing.T, members []map[string]interface{}) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/api/auth/organization/list-members" {
if r.Method != http.MethodGet || r.URL.Path != "/api/auth/organization/list-members" {
http.Error(w, "not found", http.StatusNotFound)
return
}
Expand All @@ -39,7 +39,7 @@ func makeMembersServer(t *testing.T, members []map[string]interface{}) *httptest
}))
}

func TestMembersCmd_UsesPOSTMethod(t *testing.T) {
func TestMembersCmd_UsesGETMethod(t *testing.T) {
method := ""
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/auth/organization/list-members" {
Expand All @@ -59,7 +59,7 @@ func TestMembersCmd_UsesPOSTMethod(t *testing.T) {
orgCmd.SetArgs([]string{"members"})
err := orgCmd.Execute()
require.NoError(t, err)
assert.Equal(t, http.MethodPost, method, "expected POST method for list-members endpoint")
assert.Equal(t, http.MethodGet, method, "expected GET method for list-members endpoint")
}

func TestMembersCmd_RendersTableWithColumns(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/org/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func NewSwitchCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

baseURL := khhttp.BuildBaseURL(cfg.DefaultHost)
baseURL := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg))

// Step 1: Resolve slug to organization ID.
listReq, err := client.NewRequest(http.MethodGet, baseURL+"/api/organizations", nil)
Expand Down
2 changes: 1 addition & 1 deletion cmd/project/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NewCreateCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("building request body: %w", err)
}

url := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/projects"
url := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/projects"
req, err := client.NewRequest(http.MethodPost, url, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("building request: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions cmd/project/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func NewDeleteCmd(f *cmdutil.Factory) *cobra.Command {
}

// Fetch project name for display in the confirmation prompt.
listURL := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/projects"
listURL := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/projects"
listReq, err := client.NewRequest(http.MethodGet, listURL, nil)
if err != nil {
return fmt.Errorf("building list request: %w", err)
Expand Down Expand Up @@ -76,7 +76,7 @@ func NewDeleteCmd(f *cmdutil.Factory) *cobra.Command {
}
}

url := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/projects/" + projectID
url := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/projects/" + projectID
req, err := client.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return fmt.Errorf("building request: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/project/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewGetCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

url := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/projects"
url := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/projects"
req, err := client.NewRequest(http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("building request: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/project/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func NewListCmd(f *cmdutil.Factory) *cobra.Command {
return err
}

url := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/projects?limit=" + strconv.Itoa(limit)
url := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/projects?limit=" + strconv.Itoa(limit)

req, err := client.NewRequest(http.MethodGet, url, nil)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions cmd/protocol/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func loadProtocols(f *cmdutil.Factory, refresh bool, cmd *cobra.Command) ([]Prot
}

// Fetch fresh data
raw, fetchErr := fetchSchemas(f)
raw, fetchErr := fetchSchemas(f, cmd)
if fetchErr != nil {
if staleEntry != nil {
fmt.Fprintln(f.IOStreams.ErrOut, "Warning: using cached data (could not reach API)")
Expand All @@ -118,7 +118,7 @@ func loadProtocols(f *cmdutil.Factory, refresh bool, cmd *cobra.Command) ([]Prot
}

// fetchSchemas performs a GET /api/mcp/schemas request and returns the raw body.
func fetchSchemas(f *cmdutil.Factory) (json.RawMessage, error) {
func fetchSchemas(f *cmdutil.Factory, cmd *cobra.Command) (json.RawMessage, error) {
client, err := f.HTTPClient()
if err != nil {
return nil, fmt.Errorf("creating HTTP client: %w", err)
Expand All @@ -129,7 +129,7 @@ func fetchSchemas(f *cmdutil.Factory) (json.RawMessage, error) {
return nil, fmt.Errorf("reading config: %w", err)
}

url := khhttp.BuildBaseURL(cfg.DefaultHost) + "/api/mcp/schemas"
url := khhttp.BuildBaseURL(cmdutil.ResolveHost(cmd, cfg)) + "/api/mcp/schemas"
req, err := client.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("building request: %w", err)
Expand Down
8 changes: 2 additions & 6 deletions cmd/protocol/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -228,11 +227,8 @@ func TestListCmd_Table(t *testing.T) {
require.NoError(t, err)

out := outBuf.String()
// Check NAME and ACTIONS columns
outUpper := strings.ToUpper(out)
assert.True(t, strings.Contains(outUpper, "NAME"), "expected NAME column in output")
assert.True(t, strings.Contains(outUpper, "ACTIONS"), "expected ACTIONS column in output")
// Aave has 2 actions, Uniswap has 1
// Test streams are non-TTY, so tsvWriter outputs data rows without headers.
// Verify protocol names and action counts are present.
assert.Contains(t, out, "Aave")
assert.Contains(t, out, "2")
assert.Contains(t, out, "Uniswap")
Expand Down
8 changes: 1 addition & 7 deletions cmd/run/cancel.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,7 @@ func NewCancelCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

host, _ := cmd.Flags().GetString("host")
if host == "" {
host = cfg.DefaultHost
}
if host == "" {
host = "app.keeperhub.com"
}
host := cmdutil.ResolveHost(cmd, cfg)

// Confirmation: skip if --yes or non-TTY (auto-proceed in non-interactive mode).
yes, _ := cmd.Flags().GetBool("yes")
Expand Down
16 changes: 5 additions & 11 deletions cmd/run/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,7 @@ func NewLogsCmd(f *cmdutil.Factory) *cobra.Command {
return fmt.Errorf("reading config: %w", err)
}

host, _ := cmd.Flags().GetString("host")
if host == "" {
host = cfg.DefaultHost
}
if host == "" {
host = "app.keeperhub.com"
}
host := cmdutil.ResolveHost(cmd, cfg)

url := khhttp.BuildBaseURL(host) + "/api/workflows/executions/" + runID + "/logs"
req, err := httpClient.NewRequest(http.MethodGet, url, nil)
Expand Down Expand Up @@ -125,11 +119,11 @@ func NewLogsCmd(f *cmdutil.Factory) *cobra.Command {
}

p := output.NewPrinter(f.IOStreams, cmd)
if len(logsResp.Logs) == 0 && !p.IsJSON() {
fmt.Fprintln(f.IOStreams.Out, "No logs found.")
return nil
}
return p.PrintData(logsResp, func(tw table.Writer) {
if len(logsResp.Logs) == 0 {
fmt.Fprintf(f.IOStreams.Out, "No logs available for run %s\n", runID)
return
}
tw.AppendHeader(table.Row{"STEP", "STATUS", "DURATION", "INPUT", "OUTPUT"})
for _, log := range logsResp.Logs {
errSuffix := ""
Expand Down
2 changes: 1 addition & 1 deletion cmd/run/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func TestLogsCmd_EmptyLogs(t *testing.T) {
}

out := buf.String()
if !strings.Contains(out, "No logs available") {
if !strings.Contains(out, "No logs found.") {
t.Errorf("expected empty logs message, got: %q", out)
}
}
Expand Down
33 changes: 14 additions & 19 deletions cmd/run/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"net/http"
"time"

"github.com/jedib0t/go-pretty/v6/table"
khhttp "github.com/keeperhub/cli/internal/http"
"github.com/keeperhub/cli/internal/output"
"github.com/keeperhub/cli/pkg/cmdutil"
Expand Down Expand Up @@ -73,13 +72,7 @@ See also: kh r l, kh r cancel, kh wf run`,
return fmt.Errorf("reading config: %w", err)
}

host, _ := cmd.Flags().GetString("host")
if host == "" {
host = cfg.DefaultHost
}
if host == "" {
host = "app.keeperhub.com"
}
host := cmdutil.ResolveHost(cmd, cfg)

watch, _ := cmd.Flags().GetBool("watch")
p := output.NewPrinter(f.IOStreams, cmd)
Expand Down Expand Up @@ -115,18 +108,20 @@ See also: kh r l, kh r cancel, kh wf run`,
}

printSummary := func(status *RunStatusResponse) error {
return p.PrintData(status, func(tw table.Writer) {
tw.AppendHeader(table.Row{"FIELD", "VALUE"})
tw.AppendRow(table.Row{"Status", status.Status})
tw.AppendRow(table.Row{"Steps", fmt.Sprintf("%d/%d", status.Progress.CompletedSteps, status.Progress.TotalSteps)})
currentStep := "-"
if status.Progress.CurrentNodeName != nil {
currentStep = *status.Progress.CurrentNodeName
}
tw.AppendRow(table.Row{"Current step", currentStep})
tw.AppendRow(table.Row{"Percentage", fmt.Sprintf("%d%%", status.Progress.Percentage)})
tw.Render()
if p.IsJSON() {
return p.PrintJSON(status)
}
currentStep := "-"
if status.Progress.CurrentNodeName != nil {
currentStep = *status.Progress.CurrentNodeName
}
p.PrintKeyValue([][2]string{
{"Status", status.Status},
{"Steps", fmt.Sprintf("%d/%d", status.Progress.CompletedSteps, status.Progress.TotalSteps)},
{"Current step", currentStep},
{"Percentage", fmt.Sprintf("%d%%", status.Progress.Percentage)},
})
return nil
}

if !watch {
Expand Down
Loading
Loading