From e53e27513ad074e606025f16cd7ad816080d65db Mon Sep 17 00:00:00 2001 From: Goon Date: Wed, 8 Apr 2026 11:48:37 +0700 Subject: [PATCH 1/4] feat(cli): add remaining missing commands for full API coverage New modules: - devices: paired device list/revoke/pairing-status (WS RPC) - export/import: agents, teams, skills, MCP export/import with previews New commands added to existing modules: - agents: wake, files list/get/set - knowledge-graph: dedup scan/list/dismiss, merge - memory: index, index-all, chunks - teams tasks: delete - mcp servers: reconnect - providers: verify-embedding --- cmd/agents_files.go | 92 +++++++++ cmd/agents_wake.go | 29 +++ cmd/devices.go | 86 +++++++++ cmd/export_import.go | 309 ++++++++++++++++++++++++++++++ cmd/knowledge_graph_dedup.go | 90 +++++++++ cmd/mcp_reconnect.go | 27 +++ cmd/memory_index.go | 60 ++++++ cmd/providers_verify_embedding.go | 27 +++ cmd/teams_tasks_delete.go | 35 ++++ 9 files changed, 755 insertions(+) create mode 100644 cmd/agents_files.go create mode 100644 cmd/agents_wake.go create mode 100644 cmd/devices.go create mode 100644 cmd/export_import.go create mode 100644 cmd/knowledge_graph_dedup.go create mode 100644 cmd/mcp_reconnect.go create mode 100644 cmd/memory_index.go create mode 100644 cmd/providers_verify_embedding.go create mode 100644 cmd/teams_tasks_delete.go diff --git a/cmd/agents_files.go b/cmd/agents_files.go new file mode 100644 index 0000000..2daac74 --- /dev/null +++ b/cmd/agents_files.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var agentsFilesCmd = &cobra.Command{ + Use: "files", + Short: "Manage agent context files", +} + +var agentsFilesListCmd = &cobra.Command{ + Use: "list ", Short: "List agent files", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ws, err := newWS("cli") + if err != nil { + return err + } + if _, err := ws.Connect(); err != nil { + return err + } + defer ws.Close() + data, err := ws.Call("agents.files.list", map[string]any{"agent_id": args[0]}) + if err != nil { + return err + } + printer.Print(unmarshalList(data)) + return nil + }, +} + +var agentsFilesGetCmd = &cobra.Command{ + Use: "get ", Short: "Get agent file content", Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ws, err := newWS("cli") + if err != nil { + return err + } + if _, err := ws.Connect(); err != nil { + return err + } + defer ws.Close() + data, err := ws.Call("agents.files.get", map[string]any{"agent_id": args[0], "file_name": args[1]}) + if err != nil { + return err + } + m := unmarshalMap(data) + if content := str(m, "content"); content != "" { + fmt.Println(content) + } else { + printer.Print(m) + } + return nil + }, +} + +var agentsFilesSetCmd = &cobra.Command{ + Use: "set ", Short: "Set agent file content", Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ws, err := newWS("cli") + if err != nil { + return err + } + if _, err := ws.Connect(); err != nil { + return err + } + defer ws.Close() + contentVal, _ := cmd.Flags().GetString("content") + content, err := readContent(contentVal) + if err != nil { + return err + } + _, err = ws.Call("agents.files.set", map[string]any{ + "agent_id": args[0], "file_name": args[1], "content": content, + }) + if err != nil { + return err + } + printer.Success("File updated") + return nil + }, +} + +func init() { + agentsFilesSetCmd.Flags().String("content", "", "Content (or @filepath)") + _ = agentsFilesSetCmd.MarkFlagRequired("content") + + agentsFilesCmd.AddCommand(agentsFilesListCmd, agentsFilesGetCmd, agentsFilesSetCmd) + agentsCmd.AddCommand(agentsFilesCmd) +} diff --git a/cmd/agents_wake.go b/cmd/agents_wake.go new file mode 100644 index 0000000..81a1087 --- /dev/null +++ b/cmd/agents_wake.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "net/url" + + "github.com/spf13/cobra" +) + +var agentsWakeCmd = &cobra.Command{ + Use: "wake ", + Short: "Wake up a sleeping agent", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + _, err = c.Post("/v1/agents/"+url.PathEscape(args[0])+"/wake", nil) + if err != nil { + return err + } + printer.Success("Agent woken up") + return nil + }, +} + +func init() { + agentsCmd.AddCommand(agentsWakeCmd) +} diff --git a/cmd/devices.go b/cmd/devices.go new file mode 100644 index 0000000..e5365c3 --- /dev/null +++ b/cmd/devices.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "github.com/nextlevelbuilder/goclaw-cli/internal/output" + "github.com/nextlevelbuilder/goclaw-cli/internal/tui" + "github.com/spf13/cobra" +) + +var devicesCmd = &cobra.Command{Use: "devices", Short: "Manage paired devices"} + +var devicesListCmd = &cobra.Command{ + Use: "list", Short: "List paired devices", + RunE: func(cmd *cobra.Command, args []string) error { + ws, err := newWS("cli") + if err != nil { + return err + } + if _, err := ws.Connect(); err != nil { + return err + } + defer ws.Close() + data, err := ws.Call("device.pair.list", map[string]any{}) + if err != nil { + return err + } + if cfg.OutputFormat != "table" { + printer.Print(unmarshalList(data)) + return nil + } + tbl := output.NewTable("ID", "NAME", "USER", "STATUS", "PAIRED_AT", "LAST_SEEN") + for _, d := range unmarshalList(data) { + tbl.AddRow(str(d, "id"), str(d, "device_name"), str(d, "user_id"), + str(d, "status"), str(d, "paired_at"), str(d, "last_seen_at")) + } + printer.Print(tbl) + return nil + }, +} + +var devicesRevokeCmd = &cobra.Command{ + Use: "revoke ", Short: "Revoke a paired device", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !tui.Confirm("Revoke this device?", cfg.Yes) { + return nil + } + ws, err := newWS("cli") + if err != nil { + return err + } + if _, err := ws.Connect(); err != nil { + return err + } + defer ws.Close() + _, err = ws.Call("device.pair.revoke", map[string]any{"id": args[0]}) + if err != nil { + return err + } + printer.Success("Device revoked") + return nil + }, +} + +var devicesPairingStatusCmd = &cobra.Command{ + Use: "pairing-status", Short: "Check browser pairing status", + RunE: func(cmd *cobra.Command, args []string) error { + ws, err := newWS("cli") + if err != nil { + return err + } + if _, err := ws.Connect(); err != nil { + return err + } + defer ws.Close() + data, err := ws.Call("browser.pairing.status", map[string]any{}) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +func init() { + devicesCmd.AddCommand(devicesListCmd, devicesRevokeCmd, devicesPairingStatusCmd) + rootCmd.AddCommand(devicesCmd) +} diff --git a/cmd/export_import.go b/cmd/export_import.go new file mode 100644 index 0000000..e371e51 --- /dev/null +++ b/cmd/export_import.go @@ -0,0 +1,309 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "net/url" + "os" + + "github.com/nextlevelbuilder/goclaw-cli/internal/tui" + "github.com/spf13/cobra" +) + +var exportCmd = &cobra.Command{Use: "export", Short: "Export resources (agents, teams, skills, mcp)"} +var importCmd = &cobra.Command{Use: "import", Short: "Import resources (agents, teams, skills, mcp)"} + +// --- Agent Export/Import --- + +var exportAgentPreviewCmd = &cobra.Command{ + Use: "agent-preview ", Short: "Preview agent export", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Get("/v1/agents/" + url.PathEscape(args[0]) + "/export/preview") + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +var exportAgentCmd = &cobra.Command{ + Use: "agent ", Short: "Export an agent", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + outFile, _ := cmd.Flags().GetString("output") + resp, err := c.GetRaw("/v1/agents/" + url.PathEscape(args[0]) + "/export") + if err != nil { + return err + } + defer resp.Body.Close() + return writeExportFile(resp.Body, outFile, "agent-"+args[0]+".json") + }, +} + +var importAgentPreviewCmd = &cobra.Command{ + Use: "agent-preview ", Short: "Preview agent import", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/agents/import/preview", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +var importAgentCmd = &cobra.Command{ + Use: "agent ", Short: "Import agents from file", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !tui.Confirm("Import agents from "+args[0]+"?", cfg.Yes) { + return nil + } + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/agents/import", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +// --- Team Export/Import --- + +var exportTeamPreviewCmd = &cobra.Command{ + Use: "team-preview ", Short: "Preview team export", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Get("/v1/teams/" + url.PathEscape(args[0]) + "/export/preview") + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +var exportTeamCmd = &cobra.Command{ + Use: "team ", Short: "Export a team", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + outFile, _ := cmd.Flags().GetString("output") + resp, err := c.GetRaw("/v1/teams/" + url.PathEscape(args[0]) + "/export") + if err != nil { + return err + } + defer resp.Body.Close() + return writeExportFile(resp.Body, outFile, "team-"+args[0]+".json") + }, +} + +var importTeamCmd = &cobra.Command{ + Use: "team ", Short: "Import team from file", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !tui.Confirm("Import team from "+args[0]+"?", cfg.Yes) { + return nil + } + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/teams/import", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +// --- Skills Export/Import --- + +var exportSkillsPreviewCmd = &cobra.Command{ + Use: "skills-preview", Short: "Preview skills export", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Get("/v1/skills/export/preview") + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +var exportSkillsCmd = &cobra.Command{ + Use: "skills", Short: "Export skills", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + outFile, _ := cmd.Flags().GetString("output") + resp, err := c.GetRaw("/v1/skills/export") + if err != nil { + return err + } + defer resp.Body.Close() + return writeExportFile(resp.Body, outFile, "skills-export.json") + }, +} + +var importSkillsCmd = &cobra.Command{ + Use: "skills ", Short: "Import skills from file", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !tui.Confirm("Import skills from "+args[0]+"?", cfg.Yes) { + return nil + } + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/skills/import", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +// --- MCP Export/Import --- + +var exportMCPPreviewCmd = &cobra.Command{ + Use: "mcp-preview", Short: "Preview MCP servers export", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Get("/v1/mcp/export/preview") + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +var exportMCPCmd = &cobra.Command{ + Use: "mcp", Short: "Export MCP servers", + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + outFile, _ := cmd.Flags().GetString("output") + resp, err := c.GetRaw("/v1/mcp/export") + if err != nil { + return err + } + defer resp.Body.Close() + return writeExportFile(resp.Body, outFile, "mcp-export.json") + }, +} + +var importMCPCmd = &cobra.Command{ + Use: "mcp ", Short: "Import MCP servers from file", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !tui.Confirm("Import MCP servers from "+args[0]+"?", cfg.Yes) { + return nil + } + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/mcp/import", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +// writeExportFile saves response body to a file. +func writeExportFile(body io.Reader, outFile, defaultName string) error { + if outFile == "" { + outFile = defaultName + } + f, err := os.Create(outFile) + if err != nil { + return err + } + defer f.Close() + n, err := io.Copy(f, body) + if err != nil { + return err + } + printer.Success(fmt.Sprintf("Exported %d bytes to %s", n, outFile)) + return nil +} + +// readJSONFile reads and parses a JSON file. +func readJSONFile(path string) (any, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + var body any + if err := json.Unmarshal(data, &body); err != nil { + return nil, fmt.Errorf("invalid JSON: %w", err) + } + return body, nil +} + +func init() { + for _, c := range []*cobra.Command{exportAgentCmd, exportTeamCmd, exportSkillsCmd, exportMCPCmd} { + c.Flags().StringP("output", "f", "", "Output file path") + } + + exportCmd.AddCommand(exportAgentPreviewCmd, exportAgentCmd, exportTeamPreviewCmd, exportTeamCmd, + exportSkillsPreviewCmd, exportSkillsCmd, exportMCPPreviewCmd, exportMCPCmd) + importCmd.AddCommand(importAgentPreviewCmd, importAgentCmd, importTeamCmd, importSkillsCmd, importMCPCmd) + rootCmd.AddCommand(exportCmd, importCmd) +} diff --git a/cmd/knowledge_graph_dedup.go b/cmd/knowledge_graph_dedup.go new file mode 100644 index 0000000..05a7853 --- /dev/null +++ b/cmd/knowledge_graph_dedup.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "net/url" + + "github.com/spf13/cobra" +) + +var kgDedupCmd = &cobra.Command{Use: "dedup", Short: "Manage entity deduplication"} + +var kgDedupScanCmd = &cobra.Command{ + Use: "scan ", Short: "Scan for duplicate entities", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Post("/v1/agents/"+url.PathEscape(args[0])+"/kg/dedup/scan", nil) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +var kgDedupListCmd = &cobra.Command{ + Use: "list ", Short: "List duplicate candidates", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Get("/v1/agents/" + url.PathEscape(args[0]) + "/kg/dedup") + if err != nil { + return err + } + printer.Print(unmarshalList(data)) + return nil + }, +} + +var kgDedupDismissCmd = &cobra.Command{ + Use: "dismiss ", Short: "Dismiss duplicate suggestion", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + ids, _ := cmd.Flags().GetString("ids") + data, err := c.Post("/v1/agents/"+url.PathEscape(args[0])+"/kg/dedup/dismiss", + map[string]any{"ids": ids}) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +var kgMergeCmd = &cobra.Command{ + Use: "merge ", Short: "Merge KG entities", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + source, _ := cmd.Flags().GetString("source") + target, _ := cmd.Flags().GetString("target") + data, err := c.Post("/v1/agents/"+url.PathEscape(args[0])+"/kg/merge", + buildBody("source_id", source, "target_id", target)) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +func init() { + kgDedupDismissCmd.Flags().String("ids", "", "Comma-separated duplicate pair IDs") + _ = kgDedupDismissCmd.MarkFlagRequired("ids") + kgMergeCmd.Flags().String("source", "", "Source entity ID") + kgMergeCmd.Flags().String("target", "", "Target entity ID") + _ = kgMergeCmd.MarkFlagRequired("source") + _ = kgMergeCmd.MarkFlagRequired("target") + + kgDedupCmd.AddCommand(kgDedupScanCmd, kgDedupListCmd, kgDedupDismissCmd) + kgCmd.AddCommand(kgDedupCmd, kgMergeCmd) +} diff --git a/cmd/mcp_reconnect.go b/cmd/mcp_reconnect.go new file mode 100644 index 0000000..1b55a98 --- /dev/null +++ b/cmd/mcp_reconnect.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "net/url" + + "github.com/spf13/cobra" +) + +var mcpServersReconnectCmd = &cobra.Command{ + Use: "reconnect ", Short: "Reconnect MCP server", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + _, err = c.Post("/v1/mcp/servers/"+url.PathEscape(args[0])+"/reconnect", nil) + if err != nil { + return err + } + printer.Success("MCP server reconnecting") + return nil + }, +} + +func init() { + mcpServersCmd.AddCommand(mcpServersReconnectCmd) +} diff --git a/cmd/memory_index.go b/cmd/memory_index.go new file mode 100644 index 0000000..f3aee55 --- /dev/null +++ b/cmd/memory_index.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "net/url" + + "github.com/spf13/cobra" +) + +var memoryIndexCmd = &cobra.Command{ + Use: "index ", Short: "Index a memory document", Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + _, err = c.Post("/v1/agents/"+url.PathEscape(args[0])+"/memory/index", + map[string]any{"path": args[1]}) + if err != nil { + return err + } + printer.Success("Document indexed") + return nil + }, +} + +var memoryIndexAllCmd = &cobra.Command{ + Use: "index-all ", Short: "Index all memory documents", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + _, err = c.Post("/v1/agents/"+url.PathEscape(args[0])+"/memory/index-all", nil) + if err != nil { + return err + } + printer.Success("All documents indexed") + return nil + }, +} + +var memoryChunksCmd = &cobra.Command{ + Use: "chunks ", Short: "List memory chunks", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Get("/v1/agents/" + url.PathEscape(args[0]) + "/memory/chunks") + if err != nil { + return err + } + printer.Print(unmarshalList(data)) + return nil + }, +} + +func init() { + memoryCmd.AddCommand(memoryIndexCmd, memoryIndexAllCmd, memoryChunksCmd) +} diff --git a/cmd/providers_verify_embedding.go b/cmd/providers_verify_embedding.go new file mode 100644 index 0000000..f7d6019 --- /dev/null +++ b/cmd/providers_verify_embedding.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "net/url" + + "github.com/spf13/cobra" +) + +var providersVerifyEmbeddingCmd = &cobra.Command{ + Use: "verify-embedding ", Short: "Verify embedding API credentials", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + data, err := c.Post("/v1/providers/"+url.PathEscape(args[0])+"/verify-embedding", nil) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + +func init() { + providersCmd.AddCommand(providersVerifyEmbeddingCmd) +} diff --git a/cmd/teams_tasks_delete.go b/cmd/teams_tasks_delete.go new file mode 100644 index 0000000..51f2b22 --- /dev/null +++ b/cmd/teams_tasks_delete.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "github.com/nextlevelbuilder/goclaw-cli/internal/tui" + "github.com/spf13/cobra" +) + +var teamsTasksDeleteCmd = &cobra.Command{ + Use: "delete ", Short: "Delete a task", Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + if !tui.Confirm("Delete this task?", cfg.Yes) { + return nil + } + ws, err := newWS("cli") + if err != nil { + return err + } + if _, err := ws.Connect(); err != nil { + return err + } + defer ws.Close() + _, err = ws.Call("teams.tasks.delete", map[string]any{ + "team_id": args[0], "task_id": args[1], + }) + if err != nil { + return err + } + printer.Success("Task deleted") + return nil + }, +} + +func init() { + teamsTasksCmd.AddCommand(teamsTasksDeleteCmd) +} From 072642c7f183662d04ad00f9eae8c16fdd6d4568 Mon Sep 17 00:00:00 2001 From: Goon Date: Wed, 8 Apr 2026 11:53:50 +0700 Subject: [PATCH 2/4] fix(cli): address Claude code review findings - knowledge_graph_dedup: change --ids from String to StringSlice so API receives JSON array instead of comma-separated string - agents_files: add table output for list command (consistency) - export_import: explicit f.Close() instead of defer on write path, fix -f shorthand to -o, add importTeamPreviewCmd for symmetry - mcp_reconnect: fix success message tense to match codebase style --- cmd/agents_files.go | 11 ++++++++++- cmd/export_import.go | 30 +++++++++++++++++++++++++++--- cmd/knowledge_graph_dedup.go | 4 ++-- cmd/mcp_reconnect.go | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/cmd/agents_files.go b/cmd/agents_files.go index 2daac74..44aaea4 100644 --- a/cmd/agents_files.go +++ b/cmd/agents_files.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/nextlevelbuilder/goclaw-cli/internal/output" "github.com/spf13/cobra" ) @@ -26,7 +27,15 @@ var agentsFilesListCmd = &cobra.Command{ if err != nil { return err } - printer.Print(unmarshalList(data)) + if cfg.OutputFormat != "table" { + printer.Print(unmarshalList(data)) + return nil + } + tbl := output.NewTable("NAME", "SIZE", "UPDATED") + for _, f := range unmarshalList(data) { + tbl.AddRow(str(f, "name"), str(f, "size"), str(f, "updated_at")) + } + printer.Print(tbl) return nil }, } diff --git a/cmd/export_import.go b/cmd/export_import.go index e371e51..d8120d1 100644 --- a/cmd/export_import.go +++ b/cmd/export_import.go @@ -127,6 +127,26 @@ var exportTeamCmd = &cobra.Command{ }, } +var importTeamPreviewCmd = &cobra.Command{ + Use: "team-preview ", Short: "Preview team import", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/teams/import/preview", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + var importTeamCmd = &cobra.Command{ Use: "team ", Short: "Import team from file", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -275,11 +295,14 @@ func writeExportFile(body io.Reader, outFile, defaultName string) error { if err != nil { return err } - defer f.Close() n, err := io.Copy(f, body) if err != nil { + f.Close() return err } + if err := f.Close(); err != nil { + return fmt.Errorf("close export file: %w", err) + } printer.Success(fmt.Sprintf("Exported %d bytes to %s", n, outFile)) return nil } @@ -299,11 +322,12 @@ func readJSONFile(path string) (any, error) { func init() { for _, c := range []*cobra.Command{exportAgentCmd, exportTeamCmd, exportSkillsCmd, exportMCPCmd} { - c.Flags().StringP("output", "f", "", "Output file path") + c.Flags().StringP("output", "o", "", "Output file path") } exportCmd.AddCommand(exportAgentPreviewCmd, exportAgentCmd, exportTeamPreviewCmd, exportTeamCmd, exportSkillsPreviewCmd, exportSkillsCmd, exportMCPPreviewCmd, exportMCPCmd) - importCmd.AddCommand(importAgentPreviewCmd, importAgentCmd, importTeamCmd, importSkillsCmd, importMCPCmd) + importCmd.AddCommand(importAgentPreviewCmd, importAgentCmd, importTeamPreviewCmd, importTeamCmd, + importSkillsCmd, importMCPCmd) rootCmd.AddCommand(exportCmd, importCmd) } diff --git a/cmd/knowledge_graph_dedup.go b/cmd/knowledge_graph_dedup.go index 05a7853..80459cf 100644 --- a/cmd/knowledge_graph_dedup.go +++ b/cmd/knowledge_graph_dedup.go @@ -47,7 +47,7 @@ var kgDedupDismissCmd = &cobra.Command{ if err != nil { return err } - ids, _ := cmd.Flags().GetString("ids") + ids, _ := cmd.Flags().GetStringSlice("ids") data, err := c.Post("/v1/agents/"+url.PathEscape(args[0])+"/kg/dedup/dismiss", map[string]any{"ids": ids}) if err != nil { @@ -78,7 +78,7 @@ var kgMergeCmd = &cobra.Command{ } func init() { - kgDedupDismissCmd.Flags().String("ids", "", "Comma-separated duplicate pair IDs") + kgDedupDismissCmd.Flags().StringSlice("ids", nil, "Duplicate pair IDs (comma-separated or repeated)") _ = kgDedupDismissCmd.MarkFlagRequired("ids") kgMergeCmd.Flags().String("source", "", "Source entity ID") kgMergeCmd.Flags().String("target", "", "Target entity ID") diff --git a/cmd/mcp_reconnect.go b/cmd/mcp_reconnect.go index 1b55a98..d5d732f 100644 --- a/cmd/mcp_reconnect.go +++ b/cmd/mcp_reconnect.go @@ -17,7 +17,7 @@ var mcpServersReconnectCmd = &cobra.Command{ if err != nil { return err } - printer.Success("MCP server reconnecting") + printer.Success("MCP server reconnect triggered") return nil }, } From 74a1f1c33d24d00c2c5edac800437b4dacc6c9ef Mon Sep 17 00:00:00 2001 From: Goon Date: Wed, 8 Apr 2026 11:58:12 +0700 Subject: [PATCH 3/4] fix(cli): address round 2 Claude review findings - knowledge_graph_dedup: add tui.Confirm for merge (destructive op), add table output for dedup list command - memory_index: add table output for chunks list command - export_import: add importSkillsPreviewCmd and importMCPPreviewCmd for full import preview parity with export side --- cmd/export_import.go | 42 +++++++++++++++++++++++++++++++++++- cmd/knowledge_graph_dedup.go | 15 ++++++++++++- cmd/memory_index.go | 11 +++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/cmd/export_import.go b/cmd/export_import.go index d8120d1..a0ceeb6 100644 --- a/cmd/export_import.go +++ b/cmd/export_import.go @@ -205,6 +205,26 @@ var exportSkillsCmd = &cobra.Command{ }, } +var importSkillsPreviewCmd = &cobra.Command{ + Use: "skills-preview ", Short: "Preview skills import", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/skills/import/preview", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + var importSkillsCmd = &cobra.Command{ Use: "skills ", Short: "Import skills from file", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -263,6 +283,26 @@ var exportMCPCmd = &cobra.Command{ }, } +var importMCPPreviewCmd = &cobra.Command{ + Use: "mcp-preview ", Short: "Preview MCP servers import", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newHTTP() + if err != nil { + return err + } + body, err := readJSONFile(args[0]) + if err != nil { + return err + } + data, err := c.Post("/v1/mcp/import/preview", body) + if err != nil { + return err + } + printer.Print(unmarshalMap(data)) + return nil + }, +} + var importMCPCmd = &cobra.Command{ Use: "mcp ", Short: "Import MCP servers from file", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -328,6 +368,6 @@ func init() { exportCmd.AddCommand(exportAgentPreviewCmd, exportAgentCmd, exportTeamPreviewCmd, exportTeamCmd, exportSkillsPreviewCmd, exportSkillsCmd, exportMCPPreviewCmd, exportMCPCmd) importCmd.AddCommand(importAgentPreviewCmd, importAgentCmd, importTeamPreviewCmd, importTeamCmd, - importSkillsCmd, importMCPCmd) + importSkillsPreviewCmd, importSkillsCmd, importMCPPreviewCmd, importMCPCmd) rootCmd.AddCommand(exportCmd, importCmd) } diff --git a/cmd/knowledge_graph_dedup.go b/cmd/knowledge_graph_dedup.go index 80459cf..58b9f84 100644 --- a/cmd/knowledge_graph_dedup.go +++ b/cmd/knowledge_graph_dedup.go @@ -3,6 +3,8 @@ package cmd import ( "net/url" + "github.com/nextlevelbuilder/goclaw-cli/internal/output" + "github.com/nextlevelbuilder/goclaw-cli/internal/tui" "github.com/spf13/cobra" ) @@ -35,7 +37,15 @@ var kgDedupListCmd = &cobra.Command{ if err != nil { return err } - printer.Print(unmarshalList(data)) + if cfg.OutputFormat != "table" { + printer.Print(unmarshalList(data)) + return nil + } + tbl := output.NewTable("ID", "ENTITY_A", "ENTITY_B", "SCORE") + for _, r := range unmarshalList(data) { + tbl.AddRow(str(r, "id"), str(r, "entity_a"), str(r, "entity_b"), str(r, "score")) + } + printer.Print(tbl) return nil }, } @@ -61,6 +71,9 @@ var kgDedupDismissCmd = &cobra.Command{ var kgMergeCmd = &cobra.Command{ Use: "merge ", Short: "Merge KG entities", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if !tui.Confirm("Merge these KG entities?", cfg.Yes) { + return nil + } c, err := newHTTP() if err != nil { return err diff --git a/cmd/memory_index.go b/cmd/memory_index.go index f3aee55..7c69278 100644 --- a/cmd/memory_index.go +++ b/cmd/memory_index.go @@ -3,6 +3,7 @@ package cmd import ( "net/url" + "github.com/nextlevelbuilder/goclaw-cli/internal/output" "github.com/spf13/cobra" ) @@ -50,7 +51,15 @@ var memoryChunksCmd = &cobra.Command{ if err != nil { return err } - printer.Print(unmarshalList(data)) + if cfg.OutputFormat != "table" { + printer.Print(unmarshalList(data)) + return nil + } + tbl := output.NewTable("ID", "DOCUMENT", "CONTENT", "CREATED") + for _, ch := range unmarshalList(data) { + tbl.AddRow(str(ch, "id"), str(ch, "document_path"), str(ch, "content"), str(ch, "created_at")) + } + printer.Print(tbl) return nil }, } From 0b8417d147c84c36191e3246cd0e99bde17cc4aa Mon Sep 17 00:00:00 2001 From: Goon Date: Wed, 8 Apr 2026 12:03:30 +0700 Subject: [PATCH 4/4] fix(cli): address round 3 Claude review findings - cmd_test: add devices, export, import to test registration lists - memory_index: use SIZE instead of CONTENT in chunks table to avoid terminal overflow from long prose content --- cmd/cmd_test.go | 6 ++++++ cmd/memory_index.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 69e5569..fe77109 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -28,7 +28,10 @@ func TestAllCommandsRegistered(t *testing.T) { "credentials", "cron", "delegations", + "devices", + "export", "heartbeat", + "import", "knowledge-graph", "logs", "mcp", @@ -99,7 +102,10 @@ func TestCommandUseFields(t *testing.T) { {"credentials"}, {"cron"}, {"delegations"}, + {"devices"}, + {"export"}, {"heartbeat"}, + {"import"}, {"knowledge-graph"}, {"logs"}, {"mcp"}, diff --git a/cmd/memory_index.go b/cmd/memory_index.go index 7c69278..a24b0bf 100644 --- a/cmd/memory_index.go +++ b/cmd/memory_index.go @@ -55,9 +55,9 @@ var memoryChunksCmd = &cobra.Command{ printer.Print(unmarshalList(data)) return nil } - tbl := output.NewTable("ID", "DOCUMENT", "CONTENT", "CREATED") + tbl := output.NewTable("ID", "DOCUMENT", "SIZE", "CREATED") for _, ch := range unmarshalList(data) { - tbl.AddRow(str(ch, "id"), str(ch, "document_path"), str(ch, "content"), str(ch, "created_at")) + tbl.AddRow(str(ch, "id"), str(ch, "document_path"), str(ch, "size"), str(ch, "created_at")) } printer.Print(tbl) return nil