Add a command
Model a real walmart record and expose it as a command, a route, and a tool at once.
A walmart operation is declared once and shows up everywhere: as a CLI
subcommand, as an HTTP route under serve, as an MCP tool, and as a
walmart:// URI a host can dereference. You add one by touching three files,
and every surface updates itself. The product command is the worked example
below.
1. Model the record
In walmart/types.go, a struct describes the thing you fetch. The kit and
table struct tags decide how a host addresses it and how it prints:
type Product struct {
ID string `json:"id" kit:"id"` // the URI id
Name string `json:"name"`
Brand string `json:"brand,omitempty"`
Price float64 `json:"price,omitempty"`
Currency string `json:"currency,omitempty"`
Availability string `json:"availability,omitempty"`
Description string `json:"description,omitempty" kit:"body"` // what cat and Markdown print
URL string `json:"url,omitempty"`
}
kit:"id"marks the field that becomes the URI id.kit:"body"marks the prose thatcatand the Markdown export render.json:",omitempty"keeps a record honest: a field Walmart did not serve is absent rather than zero.
2. Fetch it
In walmart/product.go, a client method returns the record. The two-layer
client hides whether the data came from the public page or the Affiliate API
fallback:
func (c *Client) GetProduct(ctx context.Context, ref string) (*Product, error) {
id := productID(ref) // accept a bare id or an /ip/ URL
body, err := c.get(ctx, c.BaseURL+"/ip/"+id)
if err != nil {
return nil, err // ErrBlocked, ErrRateLimited, ErrNotFound flow up unchanged
}
// parse __NEXT_DATA__ into a Product ...
return p, nil
}
3. Declare the operation
In walmart/ops.go, add an input struct and a handler. The struct tags tell
kit what is a positional argument, what is an inherited flag, and where the
client is injected:
type productRef struct {
ID string `kit:"arg" help:"item id or /ip/ URL"`
Client *Client `kit:"inject"`
}
func getProduct(ctx context.Context, in productRef, emit func(*Product) error) error {
p, err := in.Client.GetProduct(ctx, in.ID)
if err != nil {
return mapErr(err)
}
return emit(p)
}
Then register it in Register in walmart/domain.go:
kit.Handle(app, kit.OpMeta{
Name: "product", Group: "read", Single: true,
Summary: "Show one product by id",
URIType: "product", Resolver: true,
Args: []kit.Arg{{Name: "id", Help: "item id or /ip/ URL"}},
}, getProduct)
That is the whole change. kit.Handle reflects the input for flags and the
output for the record shape, so the operation immediately becomes:
walmart product 5037034321 # the command
curl 'localhost:7777/v1/product/5037034321' # the route, under serve
ant get walmart://product/5037034321 # the URI dereference, via a host
Resolver ops and list ops
Two flags shape how a host treats an operation:
Single: truewithResolver: truemarks the canonical one-record fetch for aURIType. It answersant get.product,store show, andcategory showare the resolvers.List: truemarks a member-lister for a parent resource. It answersant ls. A list op emits records that are themselves addressable, so every member is a URI a host can follow.search,category browse, andcategory treedo this, each tagged with theURITypeof the members it emits (productforsearchandbrowse,categoryfortree).
Map errors to exit codes
Return through mapErr so every surface reports the same outcome with the same
exit code: the bot wall reads as need-auth (exit 4), a throttle as rate-limited
(exit 5), a missing item as not-found (exit 6):
case errors.Is(err, ErrBlocked):
return errs.NeedAuth("%s", err.Error())
case errors.Is(err, ErrRateLimited):
return errs.RateLimited("%s", err.Error())
case errors.Is(err, ErrNotFound):
return errs.NotFound("%s", err.Error())
See output formats for how records render, and resource URIs for how a host addresses them.