Replace implicit note shorthand with explicit addnote command and split README
Two changes: 1. structured-add-commands: The implicit note shorthand (kb "text") caused accidental note creation from mistyped commands. Replaced with explicit kb addnote <text> command. Root command reverts to standard Cobra behaviour. Updated examples, tests, SKILL.md, and specs. 2. split-readme-developer-docs: Moved build-from-source instructions, release process, API reference, and ROCm migration notes from README.md into a new DEVELOPER.md. README now links to DEVELOPER.md for dev workflows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -206,53 +206,3 @@ func uploadFile(client *api.Client, path, tags string) (*uploadResult, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/kb-search/kb/internal/api"
|
||||
"github.com/kb-search/kb/internal/output"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var addnoteCmd = &cobra.Command{
|
||||
Use: "addnote <text>",
|
||||
Short: "Add a text note to the knowledge base",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("requires a note text argument\n\n Usage: kb addnote \"your note text here\"")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return fmt.Errorf("accepts 1 arg but received %d — quote your note text, e.g. kb addnote \"your note text here\"", len(args))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: runAddnote,
|
||||
}
|
||||
|
||||
func init() {
|
||||
addnoteCmd.Flags().String("tags", "", "tags (comma-separated)")
|
||||
rootCmd.AddCommand(addnoteCmd)
|
||||
}
|
||||
|
||||
func runAddnote(cmd *cobra.Command, args []string) error {
|
||||
tags, _ := cmd.Flags().GetString("tags")
|
||||
client := api.NewClient()
|
||||
return submitNote(client, args[0], tags)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -11,9 +11,9 @@ var examplesCmd = &cobra.Command{
|
||||
Short: "Show common usage examples",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Print(`Quick notes:
|
||||
kb "Remember to update DNS records"
|
||||
kb "Server room is building 3" --tags ops
|
||||
fmt.Print(`Add notes:
|
||||
kb addnote "Remember to update DNS records"
|
||||
kb addnote "Server room is building 3" --tags ops
|
||||
|
||||
Add files:
|
||||
kb addfile report.pdf
|
||||
|
||||
+1
-33
@@ -3,7 +3,6 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kb-search/kb/internal/api"
|
||||
"github.com/kb-search/kb/internal/config"
|
||||
@@ -23,10 +22,9 @@ var (
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kb [\"note text\" | command]",
|
||||
Use: "kb [command]",
|
||||
Short: "kb-search CLI client",
|
||||
Long: "A CLI client for the kb-search v2 engine API.\nRun 'kb examples' for common usage patterns.",
|
||||
Args: cobra.ArbitraryArgs,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := config.Load(); err != nil {
|
||||
return err
|
||||
@@ -34,44 +32,14 @@ var rootCmd = &cobra.Command{
|
||||
config.ApplyFlags(flagEngine, flagFormat, flagAPIKey)
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return cmd.Help()
|
||||
}
|
||||
if len(args) == 1 {
|
||||
return fmt.Errorf("unknown command %q\nTo add a note, use: kb \"%s ...\" or pass multiple words", args[0], args[0])
|
||||
}
|
||||
note := strings.Join(args, " ")
|
||||
tags, _ := cmd.Flags().GetString("tags")
|
||||
client := api.NewClient()
|
||||
return submitNote(client, note, tags)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
api.SetVersionInfo(Version, MinEngineVersion)
|
||||
rootCmd.Version = Version
|
||||
rootCmd.SetUsageTemplate(`Quick note taking (must be more than one word):
|
||||
kb "note text here" [flags]
|
||||
|
||||
Normal usage:
|
||||
kb [command] [flags]{{if .HasAvailableSubCommands}}
|
||||
|
||||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
||||
|
||||
Use "{{.CommandPath}} [command] --help" for more information about a command.
|
||||
`)
|
||||
rootCmd.PersistentFlags().StringVar(&flagEngine, "engine", "", "engine API URL")
|
||||
rootCmd.PersistentFlags().StringVar(&flagFormat, "format", "", "output format (human|json)")
|
||||
rootCmd.PersistentFlags().StringVar(&flagAPIKey, "api-key", "", "API key for authentication")
|
||||
rootCmd.Flags().String("tags", "", "tags for note shorthand (comma-separated)")
|
||||
}
|
||||
|
||||
// Execute runs the root command.
|
||||
|
||||
+45
-30
@@ -6,36 +6,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRootCmd_SingleWordRejected(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"infow"})
|
||||
|
||||
var stderr bytes.Buffer
|
||||
rootCmd.SetErr(&stderr)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for single bare word, got nil")
|
||||
}
|
||||
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, `unknown command "infow"`) {
|
||||
t.Errorf("expected error to mention unknown command, got: %s", errMsg)
|
||||
}
|
||||
if !strings.Contains(errMsg, "multiple words") {
|
||||
t.Errorf("expected error to suggest multiple words, got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCmd_MultipleWordsNotRejected(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"remember", "to", "update", "dns"})
|
||||
|
||||
err := rootCmd.Execute()
|
||||
// Will fail at API call (no server), but should NOT be the "unknown command" error
|
||||
if err != nil && strings.Contains(err.Error(), "unknown command") {
|
||||
t.Errorf("multi-word input should not be rejected as unknown command, got: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCmd_NoArgs_ShowsHelp(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{})
|
||||
|
||||
@@ -52,3 +22,48 @@ func TestRootCmd_NoArgs_ShowsHelp(t *testing.T) {
|
||||
t.Errorf("expected help output, got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCmd_UnknownCommand_ReturnsError(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"notacommand"})
|
||||
|
||||
var stderr bytes.Buffer
|
||||
rootCmd.SetErr(&stderr)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown command, got nil")
|
||||
}
|
||||
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "unknown command") {
|
||||
t.Errorf("expected 'unknown command' error, got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddnoteCmd_NoArgs_ReturnsError(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"addnote"})
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for addnote with no args, got nil")
|
||||
}
|
||||
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "requires a note text argument") {
|
||||
t.Errorf("expected 'requires a note text argument' error, got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddnoteCmd_TooManyArgs_ReturnsError(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"addnote", "hello", "world"})
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for addnote with too many args, got nil")
|
||||
}
|
||||
|
||||
errMsg := err.Error()
|
||||
if !strings.Contains(errMsg, "quote your note text") {
|
||||
t.Errorf("expected 'accepts 1 arg' error, got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user