feat(knowledge-base): wrap KnowledgeBase API in the CLI
Add `hf knowledge-base` group: list/get/tree/topics, create/update/delete, link/unlink to projects, and add/update/delete for topics, categories and facts. Mirrors the project command style (flag parsing, JSON/table output, token resolution). Registered in the dispatcher and the help surface, gated on the knowledge-base.* permissions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -216,6 +216,9 @@ func handleGroup(group help.Group, args []string) {
|
|||||||
case "project":
|
case "project":
|
||||||
handleProjectCommand(sub.Name, remaining)
|
handleProjectCommand(sub.Name, remaining)
|
||||||
return
|
return
|
||||||
|
case "knowledge-base":
|
||||||
|
handleKnowledgeBaseCommand(sub.Name, remaining)
|
||||||
|
return
|
||||||
case "milestone":
|
case "milestone":
|
||||||
handleMilestoneCommand(sub.Name, remaining)
|
handleMilestoneCommand(sub.Name, remaining)
|
||||||
return
|
return
|
||||||
@@ -731,6 +734,83 @@ func handleProjectCommand(subCmd string, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleKnowledgeBaseCommand(subCmd string, args []string) {
|
||||||
|
tokenFlag := ""
|
||||||
|
var filtered []string
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
switch args[i] {
|
||||||
|
case "--token":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
i++
|
||||||
|
tokenFlag = args[i]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
filtered = append(filtered, args[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
needArg := func(usage string) {
|
||||||
|
if len(filtered) < 1 {
|
||||||
|
output.Error(usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch subCmd {
|
||||||
|
case "list":
|
||||||
|
commands.RunKnowledgeBaseList(filtered, tokenFlag)
|
||||||
|
case "get":
|
||||||
|
needArg("usage: hf knowledge-base get <kb-code>")
|
||||||
|
commands.RunKnowledgeBaseGet(filtered[0], tokenFlag)
|
||||||
|
case "create":
|
||||||
|
commands.RunKnowledgeBaseCreate(filtered, tokenFlag)
|
||||||
|
case "update":
|
||||||
|
needArg("usage: hf knowledge-base update <kb-code> [--title ...] [--desc ...]")
|
||||||
|
commands.RunKnowledgeBaseUpdate(filtered[0], filtered[1:], tokenFlag)
|
||||||
|
case "delete":
|
||||||
|
needArg("usage: hf knowledge-base delete <kb-code>")
|
||||||
|
commands.RunKnowledgeBaseDelete(filtered[0], tokenFlag)
|
||||||
|
case "tree":
|
||||||
|
needArg("usage: hf knowledge-base tree <kb-code>")
|
||||||
|
commands.RunKnowledgeBaseTree(filtered[0], tokenFlag)
|
||||||
|
case "link":
|
||||||
|
needArg("usage: hf knowledge-base link <kb-code> --project <project-code>")
|
||||||
|
commands.RunKnowledgeBaseLink(filtered[0], filtered[1:], tokenFlag)
|
||||||
|
case "unlink":
|
||||||
|
needArg("usage: hf knowledge-base unlink <kb-code> --project <project-code>")
|
||||||
|
commands.RunKnowledgeBaseUnlink(filtered[0], filtered[1:], tokenFlag)
|
||||||
|
case "topics":
|
||||||
|
needArg("usage: hf knowledge-base topics <kb-code>")
|
||||||
|
commands.RunKnowledgeBaseTopics(filtered[0], tokenFlag)
|
||||||
|
case "add-topic":
|
||||||
|
needArg("usage: hf knowledge-base add-topic <kb-code> --topic <name> [--desc ...]")
|
||||||
|
commands.RunKnowledgeBaseAddTopic(filtered[0], filtered[1:], tokenFlag)
|
||||||
|
case "update-topic":
|
||||||
|
needArg("usage: hf knowledge-base update-topic <topic-id> [--topic ...] [--desc ...]")
|
||||||
|
commands.RunKnowledgeBaseUpdateTopic(filtered[0], filtered[1:], tokenFlag)
|
||||||
|
case "delete-topic":
|
||||||
|
needArg("usage: hf knowledge-base delete-topic <topic-id>")
|
||||||
|
commands.RunKnowledgeBaseDeleteTopic(filtered[0], tokenFlag)
|
||||||
|
case "add-category":
|
||||||
|
commands.RunKnowledgeBaseAddCategory(filtered, tokenFlag)
|
||||||
|
case "update-category":
|
||||||
|
needArg("usage: hf knowledge-base update-category <category-id> [--name ...] [--parent ...] [--desc ...]")
|
||||||
|
commands.RunKnowledgeBaseUpdateCategory(filtered[0], filtered[1:], tokenFlag)
|
||||||
|
case "delete-category":
|
||||||
|
needArg("usage: hf knowledge-base delete-category <category-id>")
|
||||||
|
commands.RunKnowledgeBaseDeleteCategory(filtered[0], tokenFlag)
|
||||||
|
case "add-fact":
|
||||||
|
commands.RunKnowledgeBaseAddFact(filtered, tokenFlag)
|
||||||
|
case "update-fact":
|
||||||
|
needArg("usage: hf knowledge-base update-fact <fact-id> [--fact ...] [--category ...]")
|
||||||
|
commands.RunKnowledgeBaseUpdateFact(filtered[0], filtered[1:], tokenFlag)
|
||||||
|
case "delete-fact":
|
||||||
|
needArg("usage: hf knowledge-base delete-fact <fact-id>")
|
||||||
|
commands.RunKnowledgeBaseDeleteFact(filtered[0], tokenFlag)
|
||||||
|
default:
|
||||||
|
output.Errorf("hf knowledge-base %s is not implemented yet", subCmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handleMeetingCommand(subCmd string, args []string) {
|
func handleMeetingCommand(subCmd string, args []string) {
|
||||||
tokenFlag := ""
|
tokenFlag := ""
|
||||||
var filtered []string
|
var filtered []string
|
||||||
|
|||||||
658
internal/commands/knowledge_base.go
Normal file
658
internal/commands/knowledge_base.go
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
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 <project-code>]`.
|
||||||
|
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 <kb-code>`.
|
||||||
|
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 <t> [--desc <d>]`.
|
||||||
|
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 <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
|
||||||
|
}
|
||||||
@@ -84,6 +84,30 @@ func CommandSurface() []Group {
|
|||||||
{Name: "remove-member", Description: "Remove a project member", Permitted: has(perms, "project.manage_members")},
|
{Name: "remove-member", Description: "Remove a project member", Permitted: has(perms, "project.manage_members")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "knowledge-base",
|
||||||
|
Description: "Manage knowledge bases (topics, categories, facts) and project links",
|
||||||
|
SubCommands: []Command{
|
||||||
|
{Name: "list", Description: "List knowledge bases", Permitted: has(perms, "knowledge-base.read")},
|
||||||
|
{Name: "get", Description: "Show a knowledge base by code", Permitted: has(perms, "knowledge-base.read")},
|
||||||
|
{Name: "tree", Description: "Show the full topic/category/fact tree", Permitted: has(perms, "knowledge-base.read")},
|
||||||
|
{Name: "topics", Description: "List topics in a knowledge base", Permitted: has(perms, "knowledge-base.read")},
|
||||||
|
{Name: "create", Description: "Create a knowledge base", Permitted: has(perms, "knowledge-base.create")},
|
||||||
|
{Name: "update", Description: "Update a knowledge base", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "delete", Description: "Delete a knowledge base", Permitted: has(perms, "knowledge-base.delete")},
|
||||||
|
{Name: "link", Description: "Link a knowledge base to a project", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "unlink", Description: "Unlink a knowledge base from a project", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "add-topic", Description: "Add a topic", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "update-topic", Description: "Update a topic", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "delete-topic", Description: "Delete a topic", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "add-category", Description: "Add a category", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "update-category", Description: "Update a category", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "delete-category", Description: "Delete a category (and its descendants)", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "add-fact", Description: "Add a fact", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "update-fact", Description: "Update a fact", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
{Name: "delete-fact", Description: "Delete a fact", Permitted: has(perms, "knowledge-base.update")},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "milestone",
|
Name: "milestone",
|
||||||
Description: "Manage milestones",
|
Description: "Manage milestones",
|
||||||
|
|||||||
Reference in New Issue
Block a user