package commands import ( "bytes" "encoding/json" "fmt" "strings" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/client" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/config" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/output" ) // kbResponse matches the backend KnowledgeBaseResponse schema. type kbResponse struct { ID int `json:"id"` KnowledgeBaseCode string `json:"knowledge_base_code"` Title string `json:"title"` Description *string `json:"description"` CreatedBy int `json:"created_by"` CreatedAt string `json:"created_at"` LastUpdatedAt string `json:"last_updated_at"` } type kbFactNode struct { ID int `json:"id"` Fact string `json:"fact"` } type kbCategoryNode struct { ID int `json:"id"` Name string `json:"name"` Categories []kbCategoryNode `json:"categories"` Facts []kbFactNode `json:"facts"` } type kbTopicNode struct { ID int `json:"id"` Topic string `json:"topic"` Categories []kbCategoryNode `json:"categories"` Facts []kbFactNode `json:"facts"` } type kbTree struct { Title string `json:"title"` KnowledgeBaseCode string `json:"knowledge_base_code"` Topics []kbTopicNode `json:"topics"` } func kbClient(tokenFlag string) *client.Client { cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } return client.New(cfg.BaseURL, ResolveToken(tokenFlag)) } func emitJSONOr(data []byte, fallback func()) { if output.JSONMode { var raw json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { output.Errorf("invalid JSON response: %v", err) } output.PrintJSON(raw) return } fallback() } // --------------------------------------------------------------------------- // Knowledge base CRUD // --------------------------------------------------------------------------- // RunKnowledgeBaseList implements `hf knowledge-base list [--project ]`. func RunKnowledgeBaseList(args []string, tokenFlag string) { query := "" for i := 0; i < len(args); i++ { switch args[i] { case "--project": if i+1 >= len(args) { output.Error("--project requires a value") } i++ query = appendQuery(query, "project", args[i]) default: output.Errorf("unknown flag: %s", args[i]) } } c := kbClient(tokenFlag) path := "/knowledge-bases" if query != "" { path += "?" + query } data, err := c.Get(path) if err != nil { output.Errorf("failed to list knowledge bases: %v", err) } emitJSONOr(data, func() { var kbs []kbResponse if err := json.Unmarshal(data, &kbs); err != nil { output.Errorf("cannot parse knowledge base list: %v", err) } headers := []string{"CODE", "TITLE", "DESCRIPTION"} var rows [][]string for _, k := range kbs { desc := "" if k.Description != nil { desc = *k.Description if len(desc) > 50 { desc = desc[:47] + "..." } } rows = append(rows, []string{k.KnowledgeBaseCode, k.Title, desc}) } output.PrintTable(headers, rows) }) } // RunKnowledgeBaseGet implements `hf knowledge-base get `. func RunKnowledgeBaseGet(code, tokenFlag string) { c := kbClient(tokenFlag) data, err := c.Get("/knowledge-bases/" + code) if err != nil { output.Errorf("failed to get knowledge base: %v", err) } emitJSONOr(data, func() { var k kbResponse if err := json.Unmarshal(data, &k); err != nil { output.Errorf("cannot parse knowledge base: %v", err) } desc := "" if k.Description != nil { desc = *k.Description } output.PrintKeyValue( "code", k.KnowledgeBaseCode, "title", k.Title, "description", desc, "created", k.CreatedAt, "updated", k.LastUpdatedAt, ) }) } // RunKnowledgeBaseCreate implements `hf knowledge-base create --title [--desc ]`. func RunKnowledgeBaseCreate(args []string, tokenFlag string) { title, desc := "", "" for i := 0; i < len(args); i++ { switch args[i] { case "--title": if i+1 >= len(args) { output.Error("--title requires a value") } i++ title = args[i] case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ desc = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if title == "" { output.Error("usage: hf knowledge-base create --title [--desc <description>]") } payload := map[string]interface{}{"title": title} if desc != "" { payload["description"] = desc } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) data, err := c.Post("/knowledge-bases", bytes.NewReader(body)) if err != nil { output.Errorf("failed to create knowledge base: %v", err) } emitJSONOr(data, func() { var k kbResponse if err := json.Unmarshal(data, &k); err != nil { fmt.Printf("knowledge base created: %s\n", title) return } fmt.Printf("knowledge base created: %s (code: %s)\n", k.Title, k.KnowledgeBaseCode) }) } // RunKnowledgeBaseUpdate implements `hf knowledge-base update <kb-code> [--title ...] [--desc ...]`. func RunKnowledgeBaseUpdate(code string, args []string, tokenFlag string) { payload := make(map[string]interface{}) for i := 0; i < len(args); i++ { switch args[i] { case "--title": if i+1 >= len(args) { output.Error("--title requires a value") } i++ payload["title"] = args[i] case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ payload["description"] = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if len(payload) == 0 { output.Error("nothing to update — provide --title and/or --desc") } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) _, err := c.Patch("/knowledge-bases/"+code, bytes.NewReader(body)) if err != nil { output.Errorf("failed to update knowledge base: %v", err) } fmt.Printf("knowledge base updated: %s\n", code) } // RunKnowledgeBaseDelete implements `hf knowledge-base delete <kb-code>`. func RunKnowledgeBaseDelete(code, tokenFlag string) { c := kbClient(tokenFlag) _, err := c.Delete("/knowledge-bases/" + code) if err != nil { output.Errorf("failed to delete knowledge base: %v", err) } fmt.Printf("knowledge base deleted: %s\n", code) } // RunKnowledgeBaseTree implements `hf knowledge-base tree <kb-code>`. func RunKnowledgeBaseTree(code, tokenFlag string) { c := kbClient(tokenFlag) data, err := c.Get("/knowledge-bases/" + code + "/tree") if err != nil { output.Errorf("failed to get knowledge base tree: %v", err) } emitJSONOr(data, func() { var tree kbTree if err := json.Unmarshal(data, &tree); err != nil { output.Errorf("cannot parse tree: %v", err) } fmt.Printf("%s (%s)\n", tree.Title, tree.KnowledgeBaseCode) for _, t := range tree.Topics { fmt.Printf(" # %s [topic:%d]\n", t.Topic, t.ID) for _, f := range t.Facts { fmt.Printf(" - %s [fact:%d]\n", f.Fact, f.ID) } for _, cat := range t.Categories { printKBCategory(cat, 2) } } }) } func printKBCategory(cat kbCategoryNode, depth int) { indent := strings.Repeat(" ", depth) fmt.Printf("%s> %s [category:%d]\n", indent, cat.Name, cat.ID) for _, f := range cat.Facts { fmt.Printf("%s - %s [fact:%d]\n", indent, f.Fact, f.ID) } for _, child := range cat.Categories { printKBCategory(child, depth+1) } } // --------------------------------------------------------------------------- // Project links // --------------------------------------------------------------------------- // RunKnowledgeBaseLink implements `hf knowledge-base link <kb-code> --project <project-code>`. func RunKnowledgeBaseLink(code string, args []string, tokenFlag string) { project := "" for i := 0; i < len(args); i++ { switch args[i] { case "--project": if i+1 >= len(args) { output.Error("--project requires a value") } i++ project = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if project == "" { output.Error("usage: hf knowledge-base link <kb-code> --project <project-code>") } body, _ := json.Marshal(map[string]interface{}{"knowledge_base": code}) c := kbClient(tokenFlag) _, err := c.Post("/projects/"+project+"/knowledge-bases", bytes.NewReader(body)) if err != nil { output.Errorf("failed to link knowledge base: %v", err) } fmt.Printf("knowledge base %s linked to project %s\n", code, project) } // RunKnowledgeBaseUnlink implements `hf knowledge-base unlink <kb-code> --project <project-code>`. func RunKnowledgeBaseUnlink(code string, args []string, tokenFlag string) { project := "" for i := 0; i < len(args); i++ { switch args[i] { case "--project": if i+1 >= len(args) { output.Error("--project requires a value") } i++ project = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if project == "" { output.Error("usage: hf knowledge-base unlink <kb-code> --project <project-code>") } c := kbClient(tokenFlag) _, err := c.Delete("/projects/" + project + "/knowledge-bases/" + code) if err != nil { output.Errorf("failed to unlink knowledge base: %v", err) } fmt.Printf("knowledge base %s unlinked from project %s\n", code, project) } // --------------------------------------------------------------------------- // Topics // --------------------------------------------------------------------------- // RunKnowledgeBaseTopics implements `hf knowledge-base topics <kb-code>`. func RunKnowledgeBaseTopics(code, tokenFlag string) { c := kbClient(tokenFlag) data, err := c.Get("/knowledge-bases/" + code + "/topics") if err != nil { output.Errorf("failed to list topics: %v", err) } emitJSONOr(data, func() { var topics []struct { ID int `json:"id"` Topic string `json:"topic"` Description *string `json:"description"` } if err := json.Unmarshal(data, &topics); err != nil { output.Errorf("cannot parse topics: %v", err) } headers := []string{"ID", "TOPIC", "DESCRIPTION"} var rows [][]string for _, t := range topics { desc := "" if t.Description != nil { desc = *t.Description } rows = append(rows, []string{fmt.Sprintf("%d", t.ID), t.Topic, desc}) } output.PrintTable(headers, rows) }) } // RunKnowledgeBaseAddTopic implements `hf knowledge-base add-topic <kb-code> --topic <name> [--desc ...]`. func RunKnowledgeBaseAddTopic(code string, args []string, tokenFlag string) { topic, desc := "", "" for i := 0; i < len(args); i++ { switch args[i] { case "--topic": if i+1 >= len(args) { output.Error("--topic requires a value") } i++ topic = args[i] case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ desc = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if topic == "" { output.Error("usage: hf knowledge-base add-topic <kb-code> --topic <name> [--desc <description>]") } payload := map[string]interface{}{"topic": topic} if desc != "" { payload["description"] = desc } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) data, err := c.Post("/knowledge-bases/"+code+"/topics", bytes.NewReader(body)) if err != nil { output.Errorf("failed to add topic: %v", err) } emitJSONOr(data, func() { fmt.Printf("topic added: %s\n", topic) }) } // RunKnowledgeBaseUpdateTopic implements `hf knowledge-base update-topic <topic-id> [--topic ...] [--desc ...]`. func RunKnowledgeBaseUpdateTopic(topicID string, args []string, tokenFlag string) { payload := make(map[string]interface{}) for i := 0; i < len(args); i++ { switch args[i] { case "--topic": if i+1 >= len(args) { output.Error("--topic requires a value") } i++ payload["topic"] = args[i] case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ payload["description"] = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if len(payload) == 0 { output.Error("nothing to update — provide --topic and/or --desc") } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) _, err := c.Patch("/knowledge-topics/"+topicID, bytes.NewReader(body)) if err != nil { output.Errorf("failed to update topic: %v", err) } fmt.Printf("topic updated: %s\n", topicID) } // RunKnowledgeBaseDeleteTopic implements `hf knowledge-base delete-topic <topic-id>`. func RunKnowledgeBaseDeleteTopic(topicID, tokenFlag string) { c := kbClient(tokenFlag) _, err := c.Delete("/knowledge-topics/" + topicID) if err != nil { output.Errorf("failed to delete topic: %v", err) } fmt.Printf("topic deleted: %s\n", topicID) } // --------------------------------------------------------------------------- // Categories // --------------------------------------------------------------------------- // RunKnowledgeBaseAddCategory implements // `hf knowledge-base add-category --topic <id> --name <n> [--parent <id>] [--desc ...]`. func RunKnowledgeBaseAddCategory(args []string, tokenFlag string) { name, desc := "", "" var topicID, parentID *int for i := 0; i < len(args); i++ { switch args[i] { case "--topic": if i+1 >= len(args) { output.Error("--topic requires a value") } i++ topicID = parseIntFlag("--topic", args[i]) case "--parent": if i+1 >= len(args) { output.Error("--parent requires a value") } i++ parentID = parseIntFlag("--parent", args[i]) case "--name": if i+1 >= len(args) { output.Error("--name requires a value") } i++ name = args[i] case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ desc = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if topicID == nil || name == "" { output.Error("usage: hf knowledge-base add-category --topic <topic-id> --name <name> [--parent <category-id>] [--desc <description>]") } payload := map[string]interface{}{"topic_id": *topicID, "name": name} if parentID != nil { payload["parent"] = *parentID } if desc != "" { payload["description"] = desc } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) data, err := c.Post("/knowledge-categories", bytes.NewReader(body)) if err != nil { output.Errorf("failed to add category: %v", err) } emitJSONOr(data, func() { fmt.Printf("category added: %s\n", name) }) } // RunKnowledgeBaseUpdateCategory implements // `hf knowledge-base update-category <category-id> [--name ...] [--parent ...] [--desc ...]`. func RunKnowledgeBaseUpdateCategory(categoryID string, args []string, tokenFlag string) { payload := make(map[string]interface{}) for i := 0; i < len(args); i++ { switch args[i] { case "--name": if i+1 >= len(args) { output.Error("--name requires a value") } i++ payload["name"] = args[i] case "--parent": if i+1 >= len(args) { output.Error("--parent requires a value") } i++ payload["parent"] = *parseIntFlag("--parent", args[i]) case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ payload["description"] = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if len(payload) == 0 { output.Error("nothing to update — provide --name, --parent and/or --desc") } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) _, err := c.Patch("/knowledge-categories/"+categoryID, bytes.NewReader(body)) if err != nil { output.Errorf("failed to update category: %v", err) } fmt.Printf("category updated: %s\n", categoryID) } // RunKnowledgeBaseDeleteCategory implements `hf knowledge-base delete-category <category-id>`. func RunKnowledgeBaseDeleteCategory(categoryID, tokenFlag string) { c := kbClient(tokenFlag) _, err := c.Delete("/knowledge-categories/" + categoryID) if err != nil { output.Errorf("failed to delete category: %v", err) } fmt.Printf("category deleted: %s\n", categoryID) } // --------------------------------------------------------------------------- // Facts // --------------------------------------------------------------------------- // RunKnowledgeBaseAddFact implements // `hf knowledge-base add-fact --topic <id> [--category <id>] --fact <text>`. func RunKnowledgeBaseAddFact(args []string, tokenFlag string) { factText := "" var topicID, categoryID *int for i := 0; i < len(args); i++ { switch args[i] { case "--topic": if i+1 >= len(args) { output.Error("--topic requires a value") } i++ topicID = parseIntFlag("--topic", args[i]) case "--category": if i+1 >= len(args) { output.Error("--category requires a value") } i++ categoryID = parseIntFlag("--category", args[i]) case "--fact": if i+1 >= len(args) { output.Error("--fact requires a value") } i++ factText = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if topicID == nil || factText == "" { output.Error("usage: hf knowledge-base add-fact --topic <topic-id> [--category <category-id>] --fact <text>") } payload := map[string]interface{}{"topic_id": *topicID, "fact": factText} if categoryID != nil { payload["category_id"] = *categoryID } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) data, err := c.Post("/knowledge-facts", bytes.NewReader(body)) if err != nil { output.Errorf("failed to add fact: %v", err) } emitJSONOr(data, func() { fmt.Println("fact added") }) } // RunKnowledgeBaseUpdateFact implements // `hf knowledge-base update-fact <fact-id> [--fact ...] [--category ...]`. func RunKnowledgeBaseUpdateFact(factID string, args []string, tokenFlag string) { payload := make(map[string]interface{}) for i := 0; i < len(args); i++ { switch args[i] { case "--fact": if i+1 >= len(args) { output.Error("--fact requires a value") } i++ payload["fact"] = args[i] case "--category": if i+1 >= len(args) { output.Error("--category requires a value") } i++ payload["category_id"] = *parseIntFlag("--category", args[i]) default: output.Errorf("unknown flag: %s", args[i]) } } if len(payload) == 0 { output.Error("nothing to update — provide --fact and/or --category") } body, _ := json.Marshal(payload) c := kbClient(tokenFlag) _, err := c.Patch("/knowledge-facts/"+factID, bytes.NewReader(body)) if err != nil { output.Errorf("failed to update fact: %v", err) } fmt.Printf("fact updated: %s\n", factID) } // RunKnowledgeBaseDeleteFact implements `hf knowledge-base delete-fact <fact-id>`. func RunKnowledgeBaseDeleteFact(factID, tokenFlag string) { c := kbClient(tokenFlag) _, err := c.Delete("/knowledge-facts/" + factID) if err != nil { output.Errorf("failed to delete fact: %v", err) } fmt.Printf("fact deleted: %s\n", factID) } func parseIntFlag(flag, value string) *int { var n int if _, err := fmt.Sscanf(value, "%d", &n); err != nil { output.Errorf("%s requires an integer value, got %q", flag, value) } return &n }