Files
pad/internal/server/handlers_library_entry.go
T
xarmian de1beb47a9 feat(cli): pad library get + list --full + server-side category filter (TASK-1562) (#613)
CLI layer for PLAN-1560 (`pad_library` MCP tool + matching CLI surface).
Wires the HTTP work landed in TASK-1561 through to the `pad library`
subcommands.

## `pad library list` changes

- `--category` is now a server-side filter (the old client-side
  display-only skip-loop is dead and removed).
- New `--full` flag. Default JSON output for playbooks now returns the
  `summary` field (first non-heading paragraph, ~240 char cap) instead
  of the full `content`; `--full` opts back into full bodies for
  callers that want to pipe everything.
- Table output gains a summary hint line under each playbook and a
  `/pad <slug>` chip when an invocation slug is declared, so the
  library becomes self-documenting as a discovery surface.
- `--type` now validates explicitly instead of silently producing an
  empty list for unknown values.

## NEW `pad library get <title>`

Calls `GET /api/v1/library/entry?title=X` and renders either a
conventions card (title, category, trigger, surfaces, enforcement,
commands, body) or a playbooks card (title, category, trigger, scope,
invocation slug, argument count, body). Conventions-first precedence
matches `pad library activate`.

JSON output returns the full envelope.

404 errors return a clean `not found in library: "<title>"` message
with exit code 1.

## CLI client

- `GetConventionLibrary(category)` — pass category as a server-side
  query param.
- `GetPlaybookLibrary(category, summary)` — same plus the summary
  toggle; `summary=true` strips Content and returns Summary instead.
- NEW `GetLibraryEntry(title)` returning `*LibraryEntryResponse`.
- `LibraryPlaybook` gained an omitempty `Summary` field so a single
  type round-trips both the legacy and summary shapes.

## Drive-by

Switched `/library/entry` 400/404 from a flat `{error: "..."}` body to
the canonical `writeError(code, message)` envelope used by the rest of
the API. The CLI's `parseError` now hands back a typed `APIError` that
`pad library get` pattern-matches on `Code=="not_found"` for the clean
404 message. Updated `TestLibraryEntry_MissingTitle` and `_NotFound`
to assert the new envelope.

## Verification

go build / go vet / go test ./... all green. golangci-lint clean on
cmd/pad/..., internal/cli/..., internal/server/.... End-to-end smoke
tests via the installed binary confirmed: list summary mode, list
--full, list --category filter, get convention card, get playbook
envelope, get 404 exit-1, --type validation.

Parent: PLAN-1560. Unblocks TASK-1563 (MCP catalog wiring).
2026-05-21 16:59:57 -04:00

60 lines
1.9 KiB
Go

package server
import (
"net/http"
"github.com/PerpetualSoftware/pad/internal/collections"
)
// libraryEntryResponse is the envelope returned by the /library/entry
// endpoint — type is "convention" or "playbook" so callers can deserialize
// the entry into the right shape without inspecting fields. Only one of
// Convention or Playbook is set per response.
type libraryEntryResponse struct {
Type string `json:"type"`
Convention *collections.LibraryConvention `json:"convention,omitempty"`
Playbook *collections.LibraryPlaybook `json:"playbook,omitempty"`
}
// handleLibraryEntry returns a single library entry by exact title match.
//
// Lookup precedence — conventions first, then playbooks — mirrors the
// dispatcher's `library activate` so the two stay in lockstep: if a title
// resolves to a convention for activate, it resolves to a convention here
// too.
//
// Required query param:
// - title=<exact-title>
//
// Returns 400 if title is missing, 404 if not found in either library.
// Full body is included; this endpoint is the canonical "get one entry's
// full content" path complementing the list endpoints' summary mode.
//
// TASK-1561 / PLAN-1560. No workspace context — the library is global.
func (s *Server) handleLibraryEntry(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
if title == "" {
writeError(w, http.StatusBadRequest, "bad_request", "title query parameter is required")
return
}
if conv := collections.GetLibraryConvention(title); conv != nil {
writeJSON(w, http.StatusOK, libraryEntryResponse{
Type: "convention",
Convention: conv,
})
return
}
if pb := collections.GetLibraryPlaybook(title); pb != nil {
writeJSON(w, http.StatusOK, libraryEntryResponse{
Type: "playbook",
Playbook: pb,
})
return
}
writeError(w, http.StatusNotFound, "not_found",
"not found in convention or playbook library: "+title)
}