Reindex command, implicit note shorthand, add→addfile rename

- Add `kb reindex` command with confirmation prompt and --yes flag
- Add implicit note shorthand: `kb "my note"` submits a note directly
- Rename `add` to `addfile`, remove --note/--title/--type flags
- Add client-side file extension validation before upload
- Add `kb examples` command for common usage patterns
- Update README, SKILL.md, and main specs
- Archive completed changes and sync delta specs

BREAKING: `kb add` renamed to `kb addfile`, `kb add --note` replaced by `kb "text"`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 13:58:04 +01:00
parent 528a09ca90
commit 7f4decee26
26 changed files with 786 additions and 142 deletions
+75 -83
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"github.com/kb-search/kb/internal/api"
@@ -39,93 +40,25 @@ var supportedExts = map[string]bool{
".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,
var addfileCmd = &cobra.Command{
Use: "addfile <path>",
Short: "Upload a file or directory to the knowledge base",
Args: cobra.ExactArgs(1),
RunE: runAddfile,
}
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)
addfileCmd.Flags().String("tags", "", "tags (comma-separated)")
addfileCmd.Flags().BoolP("recursive", "r", false, "recursively add directory contents")
rootCmd.AddCommand(addfileCmd)
}
func runAdd(cmd *cobra.Command, args []string) error {
func runAddfile(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 resp.StatusCode == http.StatusConflict {
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 {
if m, ok := result.(map[string]interface{}); ok {
if docID, ok := m["document_id"].(float64); ok {
fmt.Printf("Already imported: %s (doc ID: %.0f)\n", m["title"], docID)
} else if jobID, ok := m["job_id"].(float64); ok {
fmt.Printf("Already queued: %s (job ID: %.0f)\n", m["title"], jobID)
}
}
}
return nil
}
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 {
@@ -133,8 +66,19 @@ func runAdd(cmd *cobra.Command, args []string) error {
}
if !info.IsDir() {
// Validate extension
ext := strings.ToLower(filepath.Ext(path))
if !supportedExts[ext] {
supported := make([]string, 0, len(supportedExts))
for e := range supportedExts {
supported = append(supported, e)
}
sort.Strings(supported)
return fmt.Errorf("unsupported file type %q — supported: %s", ext, strings.Join(supported, ", "))
}
// Single file upload
result, err := uploadFile(client, path, tags, docType)
result, err := uploadFile(client, path, tags)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
@@ -177,7 +121,7 @@ func runAdd(cmd *cobra.Command, args []string) error {
queued := 0
duplicates := 0
for _, f := range files {
result, err := uploadFile(client, f, tags, docType)
result, err := uploadFile(client, f, tags)
if err != nil {
fmt.Fprintf(os.Stderr, "Error uploading %s: %v\n", f, err)
continue
@@ -206,7 +150,7 @@ func runAdd(cmd *cobra.Command, args []string) error {
return nil
}
func uploadFile(client *api.Client, path, tags, docType string) (*uploadResult, error) {
func uploadFile(client *api.Client, path, tags string) (*uploadResult, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("cannot open %s: %w", path, err)
@@ -217,9 +161,6 @@ func uploadFile(client *api.Client, path, tags, docType string) (*uploadResult,
if tags != "" {
fields["tags"] = tags
}
if docType != "" {
fields["type"] = docType
}
upload := &api.FileUpload{
FieldName: "file",
@@ -264,3 +205,54 @@ func uploadFile(client *api.Client, path, tags, docType string) (*uploadResult,
}
return &uploadResult{Raw: result}, nil
}
func submitNote(client *api.Client, note, tags string) error {
fields := map[string]string{
"note": note,
}
if tags != "" {
fields["tags"] = tags
}
resp, err := client.PostMultipart("/api/v1/jobs", fields, nil)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if resp.StatusCode == http.StatusConflict {
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 {
if m, ok := result.(map[string]interface{}); ok {
if docID, ok := m["document_id"].(float64); ok {
fmt.Printf("Already imported: %s (doc ID: %.0f)\n", m["title"], docID)
} else if jobID, ok := m["job_id"].(float64); ok {
fmt.Printf("Already queued: %s (job ID: %.0f)\n", m["title"], jobID)
}
}
}
return nil
}
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
}