Files
WeKnora/cli/cmd/chunk/delete.go
T
nullkey c87e35b34b chore(cli): polish + docs sync + pre-PR audit fixes
Code-reuse polish (post-implementation review pass):
- Extract text.OneLine(maxWidth, s) helper combining preview-row
  normalization (newline/CR/tab → space) with text.Truncate's
  UTF-8-safe truncation. Replaces agent/view.go truncate1Line (ASCII
  '...' + byte-slice CJK-unsafe) and chunk/list.go singleLine.
- Lift cmdutil.OpenInput(path) for the '-' = stdin / else os.Open
  pattern shared across agent create/edit and the api command.
  Replaces agent/create.go's private openInput.
- Strip inline doc-spec parentheticals from source comments — those
  belong in commit messages and project docs, not in source where
  they rot.

Pre-PR audit fixes:
- doc upload: reject `--metadata` paired with `--from-url` as
  input.invalid_argument up-front (the URL-ingest request type has
  no metadata field server-side, so the pair would otherwise silently
  drop). Long help and CHANGELOG updated to call out the asymmetry.
- doc upload (file path): map sdk.ErrDuplicateFile sentinel to
  resource.already_exists. The sentinel arrives with no "HTTP error <n>:"
  prefix because the SDK short-circuits on file-hash before reading the
  HTTP status, so the previous WrapHTTP fall-through misclassified it
  as network.error with a misleading "check base URL reachability" hint.
  The --from-url branch already handled ErrDuplicateURL this way; this
  closes the asymmetry. Caught by e2e re-upload of an already-ingested
  file; regression test added.
- README exit-10 enumeration adds `agent delete` and `chunk delete`
  (these were missing alongside the v0.5 destructive verbs they were
  meant to gate).

Docs sync:
- cli/README.md: command tree now includes the chunk subtree; adds
  agent / chunk lines to the 5-minute quickstart; adds a "Contributing
  / Reporting issues" section pointing at the repo's SECURITY.md and
  AGENTS.md; drops third-party CLI parallels from the surface
  description.
- cli/AGENTS.md: "Command surface design SOP" gains the
  flag-vs-escape-hatch step. "CRUD command flag canon" renamed to the
  hard-required-flags pattern with the contrast (TTY-prompts-fill)
  defined inline rather than via opaque shorthand.
- cli/CHANGELOG.md: search docs case-sensitivity shift promoted to its
  own #### Breaking changes subsection. MCP doc_list filter count
  corrected from 5 to 6. Drops the bogus go.mod yaml.v3 entry (yaml.v3
  was already a dependency on main; v0.5 added zero go.mod lines).
  Replaces internal-Go identifiers (fuzzyTime, NoOptDefVal) with
  user-language and drops the § section-symbol jargon.
2026-05-16 16:56:33 +08:00

108 lines
4.0 KiB
Go

package chunkcmd
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
"github.com/Tencent/WeKnora/cli/internal/prompt"
)
// chunkDeleteFields enumerates the JSON discovery fields for `chunk delete`.
// Result payload is a tiny {id, deleted} object — mirrors `kb delete` /
// `agent delete`.
var chunkDeleteFields = []string{"id", "deleted"}
type DeleteOptions struct {
ChunkID string
DocID string // required: SDK DeleteChunk takes both ids in the route.
Yes bool // sourced from the global -y/--yes persistent flag
}
// DeleteService is the narrow SDK surface this command depends on.
type DeleteService interface {
DeleteChunk(ctx context.Context, docID, chunkID string) error
}
// deleteResult is the typed payload emitted on success in JSON mode.
type deleteResult struct {
ID string `json:"id"`
Deleted bool `json:"deleted"`
}
// Delete is NOT idempotent on a missing id — it surfaces resource.not_found
// (exit 4). Idempotent-already-true semantics are reserved for `unlink`-style
// local cleanups, not server-side resource removal. Mirrors `agent delete`
// and `kb delete`.
const chunkDeleteLong = `Permanently delete a chunk from a document.
Requires both the chunk id (positional) and the parent document id
(--doc) because the server route encodes both: DELETE /chunks/{doc}/{id}.
The CLI does not auto-resolve doc id from the chunk id because doing so
would add a round-trip and open a race with the ingest pipeline (a chunk
could move between documents between resolve and delete).
Prompts for confirmation by default when stdout is a TTY and --json is
not set. Pass -y/--yes (the global flag) to skip the prompt (required in
agent / CI / piped contexts).
Typed exit codes:
resource.not_found no chunk with the given id under that doc (exit 4)
auth.forbidden caller lacks delete permission on the chunk (exit 3)
input.confirmation_required destructive op without -y on a TTY (exit 10)
AI agents: this is a high-risk write. Without -y/--yes the CLI exits 10
and writes input.confirmation_required to stderr. NEVER auto-pass -y
without the user's explicit go-ahead — the exit-10 protocol exists
exactly to guard against unintended deletes.`
const chunkDeleteExample = ` weknora chunk delete chunk_abc --doc doc_xyz # interactive confirm
weknora chunk delete chunk_abc --doc doc_xyz -y # no prompt
weknora chunk delete chunk_abc --doc doc_xyz -y --json # bare {id, deleted:true} JSON`
// NewCmdDelete builds `weknora chunk delete <chunk-id> --doc <doc-id>`.
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
opts := &DeleteOptions{}
cmd := &cobra.Command{
Use: "delete <chunk-id> --doc <doc-id>",
Short: "Delete a chunk from a document (scoped)",
Long: chunkDeleteLong,
Example: chunkDeleteExample,
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
jopts, err := cmdutil.CheckJSONFlags(c)
if err != nil {
return err
}
opts.ChunkID = args[0]
opts.Yes, _ = c.Flags().GetBool("yes")
cli, err := f.Client()
if err != nil {
return err
}
return runDelete(c.Context(), opts, jopts, cli, f.Prompter())
},
}
cmd.Flags().StringVar(&opts.DocID, "doc", "", "Parent document id (SDK knowledge_id) the chunk lives under")
_ = cmd.MarkFlagRequired("doc")
cmdutil.AddJSONFlags(cmd, chunkDeleteFields)
return cmd
}
func runDelete(ctx context.Context, opts *DeleteOptions, jopts *cmdutil.JSONOptions, svc DeleteService, p prompt.Prompter) error {
if err := cmdutil.ConfirmDestructive(p, opts.Yes, jopts.Enabled(), "chunk", opts.ChunkID); err != nil {
return err
}
if err := svc.DeleteChunk(ctx, opts.DocID, opts.ChunkID); err != nil {
return cmdutil.WrapHTTP(err, "delete chunk %s", opts.ChunkID)
}
if jopts.Enabled() {
return jopts.Emit(iostreams.IO.Out, deleteResult{ID: opts.ChunkID, Deleted: true})
}
fmt.Fprintf(iostreams.IO.Out, "✓ Deleted chunk %s\n", opts.ChunkID)
return nil
}