Files
kb/client/cmd/add.go
T
steve 9aab79d49b v2 restructure: Go client, Docker engine, release tooling
- Remove v1 Python CLI (src/kb_search/, tests/, root pyproject.toml, uv.lock, .venv)
- Add Go client with cross-platform build (client/)
- Add FastAPI engine with NVIDIA and multi-stage ROCm Dockerfiles (engine/)
- Add VERSION files for client and engine, wired into builds
- Add release.sh for automated build, tag, release, and Docker push
- Update README with build/release docs and ROCm migration note
- Clean up .gitignore for v2 project structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:52:25 +00:00

193 lines
4.0 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/kb-search/kb/internal/api"
"github.com/kb-search/kb/internal/output"
"github.com/spf13/cobra"
)
var supportedExts = map[string]bool{
".pdf": true,
".docx": true,
".html": true,
".md": true,
".txt": true,
".py": true,
".sh": true,
".go": true,
}
var addCmd = &cobra.Command{
Use: "add <path>",
Short: "Add a document or directory to the knowledge base",
Args: cobra.MaximumNArgs(1),
RunE: runAdd,
}
func init() {
addCmd.Flags().String("tags", "", "tags (comma-separated)")
addCmd.Flags().String("type", "", "document type")
addCmd.Flags().BoolP("recursive", "r", false, "recursively add directory contents")
addCmd.Flags().String("note", "", "add a text note instead of a file")
addCmd.Flags().String("title", "", "title for the note")
rootCmd.AddCommand(addCmd)
}
func runAdd(cmd *cobra.Command, args []string) error {
tags, _ := cmd.Flags().GetString("tags")
docType, _ := cmd.Flags().GetString("type")
recursive, _ := cmd.Flags().GetBool("recursive")
note, _ := cmd.Flags().GetString("note")
title, _ := cmd.Flags().GetString("title")
client := api.NewClient()
// Note mode
if note != "" {
fields := map[string]string{
"note": note,
}
if title != "" {
fields["title"] = title
}
if tags != "" {
fields["tags"] = tags
}
if docType != "" {
fields["type"] = docType
}
resp, err := client.PostMultipart("/api/v1/jobs", fields, nil)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := api.CheckError(resp); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var result interface{}
if err := api.DecodeJSON(resp, &result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
if output.IsJSON() {
output.PrintJSON(result)
} else {
fmt.Println("Queued: note")
}
return nil
}
if len(args) == 0 {
return fmt.Errorf("path argument is required (or use --note)")
}
path := args[0]
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("cannot access %s: %w", path, err)
}
if !info.IsDir() {
// Single file upload
result, err := uploadFile(client, path, tags, docType)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if output.IsJSON() {
output.PrintJSON([]interface{}{result})
} else {
fmt.Printf("Queued: %s\n", filepath.Base(path))
}
return nil
}
// Directory upload
var files []string
walkFn := func(p string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if !recursive && p != path {
return filepath.SkipDir
}
return nil
}
ext := strings.ToLower(filepath.Ext(p))
if supportedExts[ext] {
files = append(files, p)
}
return nil
}
if err := filepath.WalkDir(path, walkFn); err != nil {
return fmt.Errorf("failed to walk directory: %w", err)
}
var results []interface{}
for _, f := range files {
result, err := uploadFile(client, f, tags, docType)
if err != nil {
fmt.Fprintf(os.Stderr, "Error uploading %s: %v\n", f, err)
continue
}
results = append(results, result)
if !output.IsJSON() {
fmt.Printf("Queued: %s\n", filepath.Base(f))
}
}
if output.IsJSON() {
output.PrintJSON(results)
} else {
fmt.Printf("Queued: %d files\n", len(results))
}
return nil
}
func uploadFile(client *api.Client, path, tags, docType string) (interface{}, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("cannot open %s: %w", path, err)
}
defer f.Close()
fields := make(map[string]string)
if tags != "" {
fields["tags"] = tags
}
if docType != "" {
fields["type"] = docType
}
upload := &api.FileUpload{
FieldName: "file",
FileName: filepath.Base(path),
Reader: f,
}
resp, err := client.PostMultipart("/api/v1/jobs", fields, upload)
if err != nil {
return nil, err
}
if err := api.CheckError(resp); err != nil {
return nil, err
}
var result interface{}
if err := api.DecodeJSON(resp, &result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return result, nil
}