kit

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

README

kit

Declare an operation once, expose it as a CLI subcommand, an HTTP route, and an MCP tool. A domain describes what its verbs do and how their inputs and outputs are shaped; kit builds the command tree, the flag parser, the JSON-over-HTTP server, and the MCP server from that one description. The domain imports kit and its own data package, and nothing else: cobra, pflag, and fang stay behind the framework.

The shape of an operation

An operation is a typed handler plus surface-neutral metadata. The handler takes a context, a typed input struct, and an emit callback; it streams zero or more records and returns an error.

type searchIn struct {
	Query  string        `kit:"arg" help:"URL or prefix to search"`
	Limit  int           `kit:"flag" default:"20" help:"max captures"`
	Client *crawl.Client `kit:"inject"`
}

type Capture struct {
	URL    string `json:"url" kit:"id"`
	Status int    `json:"status"`
}

kit.Handle(app, kit.OpMeta{
	Name:    "search",
	Group:   "read",
	Summary: "Find captures for a URL or prefix",
}, func(ctx context.Context, in searchIn, emit func(Capture) error) error {
	return in.Client.Search(ctx, in.Query, in.Limit, emit)
})

That one registration yields:

  • mybin search example.com --limit 50 on the CLI,
  • GET /search?q=example.com&limit=50 once you run mybin serve,
  • a search MCP tool once you run mybin mcp,

each rendering the same Capture stream, applying the same --limit, and mapping the same errors to the same exit codes and HTTP statuses.

kit.Handle is the only registration call a domain makes. It reflects the input struct once to build the parameter set, and the output struct to learn the record collection name and its kit:"id" primary key (used to upsert into the record store when --db is set).

Input fields

The kit struct tag says how kit fills a field:

tag meaning
kit:"arg" a positional argument, in declaration order
kit:"flag" a named flag / query parameter / tool argument (the default)
kit:"inject" filled by kit with the domain client or the record store

Options after the kind refine the binding:

option effect
name=foo override the derived snake_case name
short=q single-letter CLI shorthand (-q)
variadic an arg that takes the rest of the positionals; a slice flag
inherit bind to a framework-global flag of the same name (e.g. limit)

Sibling tags describe the value:

Mode  string `kit:"flag" short:"m" default:"text" enum:"text,html,raw" help:"output mode"`
Hosts []string `kit:"arg" variadic help:"one or more hosts"`

help is the one-line description shown in CLI help, the OpenAPI summary, and the MCP schema. default is the value used when the flag is unset. enum is a comma-separated set of allowed values.

An inject field receives the run's domain client or its record store by assignability, so a handler declares Client *crawl.Client or St store.Store and gets the right one.

Building an app

func main() {
	app := kit.New(kit.Identity{
		Binary:  "ccrawl",
		Short:   "A command line for Common Crawl",
		Version: version,
	})

	app.SetClient(func(ctx context.Context, c kit.Config) (any, error) {
		return crawl.New(c), nil
	})

	registerOps(app)
	kit.Main(app)
}
  • kit.New(Identity, ...Option) derives the baseline Config (XDG data and config dirs, env prefix, rate, retries, timeout, workers) from the binary name. kit.WithDefaults(fn) overlays the domain's own values onto that baseline; kit.EnvPrefix("CC") overrides the env var prefix.
  • app.SetClient(fn) registers the factory kit calls once per run to build the domain client from the resolved Config. The result is injected into handler kit:"inject" fields. A CLI with no shared client can skip this.
  • app.GlobalFlags(fn) registers persistent flags that are not part of the baseline (for ccrawl: --crawl, --source). The domain binds them to its own variables through a kit.FlagSet and reads them when building its client.
  • kit.Run(ctx, app) builds the CLI tree, runs it, and returns a process exit code. kit.Main(app) wraps that with a cancellable context and os.Exit.

Wire the hooks as named methods rather than closures when they need shared state, which keeps each one readable as package-level behaviour:

type builder struct {
	dom *domainGlobals
	def crawl.Config
}

func (b *builder) defaults(c *kit.Config) { c.Rate = b.def.Delay /* ... */ }
func (b *builder) globals(f *kit.FlagSet)  { f.StringVarP(&b.dom.crawl, "crawl", "c", "latest", "crawl ID") }
func (b *builder) client(_ context.Context, c kit.Config) (any, error) { return buildApp(c, b.dom), nil }

b := &builder{dom: &domainGlobals{}, def: crawl.DefaultConfig()}
app := kit.New(id, kit.WithDefaults(b.defaults))
app.GlobalFlags(b.globals)
app.SetClient(b.client)

Escape hatches

Some verbs do not fit the emit-records shape: a byte stream, an interactive shell, a bulk download, or a parent that only groups subcommands. Declare those as a kit.Command, a small struct of metadata wired to named functions:

type getCmd struct {
	mode    string
	outFile string
}

func newGetCmd() kit.Command {
	c := &getCmd{}
	return kit.Command{
		Use:   "get <url>",
		Short: "Print the bytes Common Crawl captured",
		Args:  kit.ExactArgs(1),
		Flags: c.flags,
		Run:   c.run,
	}
}

func (c *getCmd) flags(f *kit.FlagSet) {
	f.StringVarP(&c.mode, "mode", "m", "text", "output mode")
	f.StringVarP(&c.outFile, "out", "O", "", "write to a file")
}

func (c *getCmd) run(ctx context.Context, args []string) error {
	app := kit.MustClient[*App](ctx)
	return runGet(ctx, app, args[0], c.mode, c.outFile)
}

app.AddCommand(newGetCmd())
  • Command.Flags func(*FlagSet) binds flags through a kit.FlagSet, whose method set mirrors pflag: StringVar/StringVarP, StringSliceVar, IntVar/Int64Var, BoolVar, Float64Var, DurationVar, CountVar, each with a ...P shorthand variant.
  • Command.Args Args validates the positionals. Use kit.ExactArgs(n), kit.MinimumNArgs(n), kit.MaximumNArgs(n), kit.RangeArgs(min, max), or kit.NoArgs. They return a usage error, so a wrong argument count exits 2 with a message on every surface.
  • Command.Sub []Command nests subcommands. Command.Write bool marks a mutating command. Command.Group places the command in a help group.
  • app.AddCommandUnder(parent, cmd) attaches an escape hatch beneath a parent that also hosts operations, so crawls list (an op) and crawls info (an escape hatch) sit side by side.

A run handler reaches the run's resolved config, client, and record store with the context:

app := kit.MustClient[*App](ctx)        // panics on a wiring bug; for infallible factories
app, err := kit.Client[*App](ctx)       // returns the error; for factories that can fail

kit.MustClient mirrors template.Must and regexp.MustCompile: when a client factory only assembles config and shared handles, a missing or mistyped client is a wiring bug, not a runtime condition. For lower-level access, kit.FromContext(ctx) returns the full *kit.State (config, client, store, resolved output options).

Escape-hatch commands appear on the CLI only. The HTTP and MCP surfaces expose registered operations.

Surfaces and exit codes

The default invocation runs the CLI. mybin serve starts the HTTP server and mybin mcp starts the MCP server over stdio, both exposing the same operations.

Return one of the kit/errs errors to get a stable result across surfaces:

constructor exit code HTTP status meaning
(nil) 0 200 success
errs.Usage 2 400 bad arguments or flags
errs.NoResults 3 404 the stream was empty
errs.NeedAuth 4 401 a credential is required
errs.RateLimited 5 429 upstream throttled the request
errs.NotFound 6 404 the requested thing does not exist
errs.Unsupported 7 422 the operation cannot be served here
errs.Network 8 502 an upstream call failed
any other error 1 500 unclassified failure

An operation that emits nothing returns NoResults automatically (exit 3), unless its OpMeta.Single is set.

Package layout

  • kit — the registry, the surfaces, the Command builder, Client/MustClient.
  • kit/errs — the error taxonomy and its exit-code and HTTP-status mapping.
  • kit/render — the record renderer (--output table/markdown/json/jsonl/csv/tsv/url/raw/template; table and JSON are color-aware on a TTY).
  • kit/store — the optional record store written to when --db is set.

See docs.go for the package overview rendered by go doc.

Documentation

Overview

Package kit turns one declaration of an operation into three surfaces: a CLI subcommand, an HTTP route, and an MCP tool. A domain describes what its verbs do and how their inputs and outputs are shaped; kit builds the command tree, the flag parser, the JSON-over-HTTP server, and the MCP server from that one description.

The model

An operation is a typed handler with surface-neutral metadata. The handler takes a context, a typed input struct, and an emit callback; it streams zero or more records to emit and returns an error. kit reflects the input struct once to learn its arguments and flags, and the output struct to learn the record's collection name and primary key. The same operation then renders as a CLI subcommand, answers an HTTP request, and exposes an MCP tool, with no per-surface code in the domain.

type searchIn struct {
	Query  string        `kit:"arg" help:"URL or prefix to search"`
	Limit  int           `kit:"flag" default:"20" help:"max captures"`
	Client *crawl.Client `kit:"inject"`
}

type Capture struct {
	URL    string `json:"url" kit:"id"`
	Status int    `json:"status"`
}

kit.Handle(app, kit.OpMeta{
	Name:    "search",
	Group:   "read",
	Summary: "Find captures for a URL or prefix",
}, func(ctx context.Context, in searchIn, emit func(Capture) error) error {
	return in.Client.Search(ctx, in.Query, in.Limit, emit)
})

Input fields

A field's kit tag says how kit fills it:

kit:"arg"     a positional argument, in declaration order
kit:"flag"    a named flag / query parameter / tool argument (the default)
kit:"inject"  filled by kit with the domain client or record store

Options after the kind refine the binding:

name=foo      override the derived snake_case name
short=q       single-letter CLI shorthand (-q)
variadic      an arg that takes the rest of the positionals; a slice flag
inherit       bind to a framework-global flag of the same name (e.g. limit)

Sibling tags describe the value: help for the one-line description, default for the unset value, and enum for a comma-separated set of allowed values.

An output field tagged kit:"id" (or named id) is the record's primary key, used to upsert into the record store when --db is set.

Building an app

A domain builds one App, registers its operations, sets a client factory, and hands the App to Run:

func main() {
	app := kit.New(kit.Identity{
		Binary:  "ccrawl",
		Short:   "A command line for Common Crawl",
		Version: version,
	})
	app.SetClient(func(ctx context.Context, c kit.Config) (any, error) {
		return crawl.New(c), nil
	})
	registerOps(app)
	kit.Main(app)
}

New derives the baseline Config (XDG paths, env prefix, rate, retries, timeout, workers) from the binary name; WithDefaults overlays the domain's own values. SetClient registers the factory kit calls once per run; its result is injected into handler fields tagged kit:"inject" by assignability. GlobalFlags registers persistent flags that are not part of the baseline, and the domain reads them when building its client.

Escape hatches

Some verbs do not fit the emit-records shape: a byte stream, an interactive shell, a bulk download, or a parent that only groups subcommands. A domain declares those as a Command, a small struct of metadata wired to named functions, without naming the underlying cobra and pflag types:

app.AddCommand(kit.Command{
	Use:   "get <url>",
	Short: "Print the bytes Common Crawl captured",
	Args:  kit.ExactArgs(1),
	Flags: c.flags,
	Run:   c.run,
})

Command.Flags binds flags through a FlagSet, whose method set mirrors pflag (StringVar, StringVarP, IntVar, BoolVar, and so on). Command.Args validates the positionals with one of ExactArgs, MinimumNArgs, MaximumNArgs, RangeArgs, or NoArgs. A run handler reaches the run's resolved config, client, and record store with MustClient (or Client when building can fail), the typed companions to FromContext and State.Client. Escape-hatch commands appear on the CLI only; the HTTP and MCP surfaces expose registered operations.

Surfaces

The default invocation runs the CLI. The serve subcommand starts the HTTP server, mcp starts the MCP server over stdio, and each exposes the same operations. Run returns a process exit code derived from the error: the github.com/tamnd/any-cli/kit/errs taxonomy maps a usage error to 2, an empty result to 3, a missing credential to 4, a rate limit to 5, and so on, so the same failure yields the same code on every surface.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Client

func Client[T any](ctx context.Context) (T, error)

Client returns the run's domain client typed as T. It is the typed companion to State.Client: an escape-hatch command that wants the concrete app it registered with SetClient calls Client[*MyApp](ctx) instead of fetching the state and asserting the any itself.

It returns an error when there is no run state on the context, when building the client failed, or when the client is not a T.

func Domains added in v0.2.0

func Domains() []string

Domains returns the canonical schemes of all registered domains, sorted. It is the analogue of sql.Drivers().

func Handle

func Handle[In, Out any](app *App, meta OpMeta, fn func(context.Context, In, func(Out) error) error)

Handle registers a typed handler. It is the only registration call a domain makes. It reflects In to build the parameter set once, and Out to learn the record's collection name and primary key for the store tee.

func IsReservedKind added in v0.2.0

func IsReservedKind(scheme string) bool

IsReservedKind reports whether scheme is one of the cross-site kind schemes.

func Main

func Main(app *App)

Main is a convenience wrapper that runs the app with a cancellable context and calls os.Exit with the resulting code.

func MustClient

func MustClient[T any](ctx context.Context) T

MustClient is Client for clients that cannot fail to build. When a command's client factory is infallible (it only assembles config and shared handles), a missing or mistyped client is a wiring bug, not a runtime condition, so MustClient panics rather than making every command thread an impossible error. This mirrors template.Must and regexp.MustCompile.

func NoArgs

func NoArgs(args []string) error

NoArgs rejects any positional argument.

func Register added in v0.2.0

func Register(dom Domain)

Register makes a Domain available to every host in the process. It is meant to be called from a domain package's init(). It panics — like sql.Register — if dom is nil, if its scheme is empty or malformed, if the scheme shadows a reserved kind (host/pages/feed/search/data), or if the scheme or any alias collides with an already-registered domain.

func Run

func Run(ctx context.Context, app *App) int

Run is the single entrypoint a generated main calls. It builds the CLI tree from the registry, wraps it in fang for help, completion, and error rendering, and executes. The default invocation is the CLI; the serve, mcp, and tui subcommands switch surfaces. Run returns the process exit code.

func WithState

func WithState(ctx context.Context, st *State) context.Context

WithState returns a context carrying st, so child commands can retrieve it.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the registry every surface drives. A domain builds one with New, registers operations with Handle, sets its client factory, and hands the App to Run. The App holds no surface state of its own; each surface reads the operation list and the config to build itself.

func New

func New(id Identity, opts ...Option) *App

New builds an App from its identity. It derives the baseline config from the binary name, then applies any options.

func (*App) AddCommand

func (a *App) AddCommand(cmd Command)

AddCommand is the escape hatch for an operation that does not fit the emit-records shape (an interactive shell, a DuckDB analytical console, a binary download). The command joins the CLI tree as-is. It is absent from the API and MCP surfaces, which expose only registered operations.

func (*App) AddCommandUnder

func (a *App) AddCommandUnder(parent string, cmd Command)

AddCommandUnder attaches an escape-hatch command beneath a parent command, the same parent a nested operation declares with OpMeta.Parent. It lets a domain mix generated operations and hand-rolled commands under one group, so "crawls list" (an op) and "crawls info" (an escape hatch) sit side by side.

func (*App) CommandGroup

func (a *App) CommandGroup(name, summary string)

CommandGroup sets the help summary for a parent command, whether that parent hosts operations, escape-hatch commands, or both. Without it a parent shows a generated one-line summary.

func (*App) Config

func (a *App) Config() Config

Config returns the resolved baseline config, for a domain that needs to read a default while wiring its client factory.

func (*App) Finalize

func (a *App) Finalize(fn func(*Config))

Finalize registers a hook kit calls after folding the framework globals onto the config, so a domain can apply its own global flags to the resolved Config (for instance copying a --data-dir-derived path into a domain field).

func (*App) GlobalFlags

func (a *App) GlobalFlags(fn func(*FlagSet))

GlobalFlags lets a domain register its own persistent flags on the root, for settings that are not part of the framework baseline (for ccrawl: --crawl, --source, --library). The domain binds them to its own variables and reads those when building its client and its escape-hatch commands.

func (*App) Identity

func (a *App) Identity() Identity

Identity returns the app identity.

func (*App) Ops

func (a *App) Ops() []Operation

Ops returns the registered operations in registration order.

func (*App) SetClient

func (a *App) SetClient(fn func(context.Context, Config) (any, error))

SetClient registers the factory kit calls once per run to build the domain client from the resolved config. The returned value is injected into handler In fields tagged kit:"inject" by assignability. A CLI with no shared client can skip this.

type Arg

type Arg struct {
	Name     string
	Help     string
	Optional bool
	Variadic bool
}

Arg describes one positional argument for help and schema generation.

type Args

type Args func([]string) error

Args validates a command's positional arguments. The constructors below cover the common cases; any function with this signature works, and returning an errs.Usage error keeps the exit code consistent with the rest of kit.

func ExactArgs

func ExactArgs(n int) Args

ExactArgs requires exactly n positional arguments.

func MaximumNArgs

func MaximumNArgs(n int) Args

MaximumNArgs allows at most n positional arguments.

func MinimumNArgs

func MinimumNArgs(n int) Args

MinimumNArgs requires at least n positional arguments.

func RangeArgs

func RangeArgs(min, max int) Args

RangeArgs requires between min and max positional arguments, inclusive.

type Command

type Command struct {
	Use     string                                // usage line, e.g. "paths <kind>"
	Short   string                                // one-line summary
	Long    string                                // multi-paragraph help
	Aliases []string                              // alternate names
	Group   string                                // help group ID, matching an operation's Group
	Args    Args                                  // positional-argument check; nil accepts any
	Flags   func(*FlagSet)                        // binds this command's flags; nil for none
	Run     func(context.Context, []string) error // handler; nil for a parent
	Sub     []Command                             // subcommands
	Write   bool                                  // marks a mutating command (annotated for surfaces)
}

Command is a hand-written escape-hatch command, declared without naming the underlying cobra/pflag types. A domain reaches for it when a verb does not fit the emit-records shape of an operation: a byte stream, an interactive shell, a bulk download, or a parent that only groups subcommands.

The function-typed fields take named methods or functions, not closures, so a command reads as a small struct of metadata wired to package-level behaviour:

func newPathsCmd() kit.Command {
	c := &pathsCmd{}
	return kit.Command{
		Use:   "paths <kind>",
		Short: "List the archive file paths for a crawl",
		Args:  kit.MaximumNArgs(1),
		Flags: c.flags,
		Run:   c.run,
	}
}

Escape-hatch commands appear on the CLI only; the HTTP and MCP surfaces expose registered operations.

type Config

type Config struct {
	Binary    string        // the binary name, e.g. "ccrawl"
	EnvPrefix string        // env var prefix, e.g. "CCRAWL"
	DataDir   string        // root for cache, store, sessions
	CacheDir  string        // on-disk content cache
	ConfigDir string        // config file directory
	Profile   string        // selected profile name
	Rate      time.Duration // minimum delay between requests
	Retries   int           // retry attempts on 429/5xx
	Timeout   time.Duration // per-request timeout
	Workers   int           // bulk concurrency
	NoCache   bool          // bypass caches
	Color     string        // auto|always|never
	UserAgent string        // descriptive default UA
	DB        string        // --db DSN for the record store (empty = no store)
	Verbose   int           // verbosity level
	Quiet     bool          // suppress progress
	DryRun    bool          // print actions, do not perform them
	Extra     map[string]string
}

Config is the resolved runtime configuration kit hands a domain when it builds the client. The common fields live here so a domain does not re-derive XDG paths, rate, retries, and the like; a domain adds only its own extra settings (carried in Extra or read from its own env). Precedence is applied by the CLI surface: flags over env over the config file over these defaults.

func DefaultConfig

func DefaultConfig(binary, envPrefix string) Config

DefaultConfig builds the baseline config for a binary, deriving the XDG paths and env prefix from the name. A domain overlays its own defaults on top.

func (Config) Env

func (c Config) Env(key string) (string, bool)

Env reads a prefixed environment variable (e.g. CCRAWL_DATA_DIR for key "DATA_DIR"), returning ok=false when unset.

type Domain added in v0.2.0

type Domain interface {
	// Info returns the domain's identity and URI-scheme metadata.
	Info() DomainInfo
	// Register installs this domain's operations, client factory, and any
	// domain-global flags onto app. It must be deterministic and do no I/O.
	Register(app *App)
}

Domain is the driver for one site's data and its URI scheme. A site's library package registers an implementation from its init() with Register, so a blank import is enough to enable the domain — exactly as a database/sql program enables a driver with `import _ "github.com/lib/pq"`. The same Domain also builds the site's single binary (App), so the binary and a multi-domain host (ant) share one source of truth per site.

func Lookup added in v0.2.0

func Lookup(scheme string) (Domain, bool)

Lookup returns the registered domain for a scheme or one of its aliases, resolving the alias to the canonical domain. ok is false if no domain claims the scheme.

type DomainInfo added in v0.2.0

type DomainInfo struct {
	Scheme   string   // canonical URI scheme and binary slug, e.g. "goodreads"
	Aliases  []string // alternate schemes that canonicalize to Scheme, e.g. {"ytb"}
	Hosts    []string // hostnames this domain owns, e.g. {"goodreads.com"}; used to resolve a pasted https URL to this domain
	Identity Identity // reused by the single-site main to seed help/version
}

DomainInfo is the static descriptor of a domain.

type Envelope added in v0.2.0

type Envelope struct {
	ID      string              `json:"@id"`
	Type    string              `json:"@type"`
	Fetched string              `json:"@fetched,omitempty"`
	Links   map[string][]string `json:"@links,omitempty"`
	Data    any                 `json:"data"`
}

Envelope is the self-describing form a record takes when it is written to the data surface (8000_uri §6): the record's own fields under Data, wrapped with the URI that names it (@id), its type (@type), when it was fetched (@fetched), and its outbound graph edges as URIs (@links). A tree of these is walkable without the domain code that produced them, because every name is a URI.

type FlagSet

type FlagSet struct {
	// contains filtered or unexported fields
}

FlagSet binds a command's flags to a domain's own variables, the way a flag.FlagSet does, but without exposing the cobra/pflag types to the domain. The method set mirrors pflag one-for-one: each Var binds a long flag, and the matching VarP variant adds a single-letter shorthand. A domain that needs a flag type not listed here can request one; the set covers the common cases.

func (*FlagSet) BoolVar

func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string)

BoolVar binds a bool flag.

func (*FlagSet) BoolVarP

func (f *FlagSet) BoolVarP(p *bool, name, shorthand string, value bool, usage string)

BoolVarP binds a bool flag with a shorthand.

func (*FlagSet) CountVar

func (f *FlagSet) CountVar(p *int, name, usage string)

CountVar binds a repeatable counting flag (e.g. --verbose --verbose).

func (*FlagSet) CountVarP

func (f *FlagSet) CountVarP(p *int, name, shorthand, usage string)

CountVarP binds a repeatable counting flag with a shorthand (e.g. -vvv).

func (*FlagSet) DurationVar

func (f *FlagSet) DurationVar(p *time.Duration, name string, value time.Duration, usage string)

DurationVar binds a time.Duration flag (e.g. --timeout 30s).

func (*FlagSet) DurationVarP

func (f *FlagSet) DurationVarP(p *time.Duration, name, shorthand string, value time.Duration, usage string)

DurationVarP binds a time.Duration flag with a shorthand.

func (*FlagSet) Float64Var

func (f *FlagSet) Float64Var(p *float64, name string, value float64, usage string)

Float64Var binds a float64 flag.

func (*FlagSet) Float64VarP

func (f *FlagSet) Float64VarP(p *float64, name, shorthand string, value float64, usage string)

Float64VarP binds a float64 flag with a shorthand.

func (*FlagSet) Int64Var

func (f *FlagSet) Int64Var(p *int64, name string, value int64, usage string)

Int64Var binds an int64 flag.

func (*FlagSet) Int64VarP

func (f *FlagSet) Int64VarP(p *int64, name, shorthand string, value int64, usage string)

Int64VarP binds an int64 flag with a shorthand.

func (*FlagSet) IntVar

func (f *FlagSet) IntVar(p *int, name string, value int, usage string)

IntVar binds an int flag.

func (*FlagSet) IntVarP

func (f *FlagSet) IntVarP(p *int, name, shorthand string, value int, usage string)

IntVarP binds an int flag with a shorthand.

func (*FlagSet) StringSliceVar

func (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string)

StringSliceVar binds a repeatable string flag, collected into a slice.

func (*FlagSet) StringSliceVarP

func (f *FlagSet) StringSliceVarP(p *[]string, name, shorthand string, value []string, usage string)

StringSliceVarP binds a repeatable string flag with a shorthand.

func (*FlagSet) StringVar

func (f *FlagSet) StringVar(p *string, name, value, usage string)

StringVar binds a string flag.

func (*FlagSet) StringVarP

func (f *FlagSet) StringVarP(p *string, name, shorthand, value, usage string)

StringVarP binds a string flag with a shorthand.

type Globals

type Globals struct {
	Limit int // --limit / -n; 0 means no limit
}

Globals holds the framework-wide flags shared by every operation. They are parsed once by the surface and threaded through to Invoke.

type Host added in v0.2.0

type Host struct {
	// contains filtered or unexported fields
}

Host is a multi-domain resolver: it mounts every registered Domain behind its URI scheme and dereferences a resource URI to a record by routing to that domain's operations. It is the runtime ant drives, and the analogue of an *sql.DB that can talk to every registered driver at once. A Host is safe for concurrent use; clients are built lazily and cached per domain.

func Open added in v0.2.0

func Open(opts ...HostOption) (*Host, error)

Open mounts every registered domain and returns a ready Host. It builds each domain's App once (so op metadata and client factories are resolved up front) but defers building clients until a URI actually routes to that domain. It is the analogue of sql.Open, except the data source is the whole registry rather than one named driver.

func (*Host) Body added in v0.2.0

func (h *Host) Body(rec any) (string, bool)

Body returns the record's long-text body (the kit:"body" field) and whether it has one, so `ant cat` and the Markdown export can print the human-readable text without knowing which field holds it. It is pure reflection over the tag.

func (*Host) Domain added in v0.2.0

func (h *Host) Domain(scheme string) (DomainInfo, bool)

Domain returns the descriptor of a mounted domain by scheme or alias.

func (*Host) Domains added in v0.2.0

func (h *Host) Domains() []string

Domains returns the canonical schemes this host serves, sorted.

func (*Host) Get added in v0.2.0

func (h *Host) Get(ctx context.Context, u URI) (any, error)

Get dereferences a URI to its record by routing to the domain op that mints that authority's type and passing the id as the op's argument. It returns the single record the op emits.

func (h *Host) Links(rec any) []URI

Links returns the outbound graph edges of a record: one URI per kit:"link" field value, with multi-valued and optional fields handled. It is pure reflection over the record's tags, so it needs no network and no host lookup beyond canonicalizing each link's scheme.

func (*Host) List added in v0.2.0

func (h *Host) List(ctx context.Context, u URI, limit int) ([]any, error)

List returns the member records of a collection URI by routing to the domain's list op for that authority and passing the parent id. limit caps the result (0 means the op's own default).

func (*Host) Locate added in v0.2.0

func (h *Host) Locate(u URI) (string, error)

Locate returns the live https location of a URI, the inverse of Resolve. It asks the owning domain's Resolver; a domain without one cannot be located.

func (*Host) Mint added in v0.2.0

func (h *Host) Mint(rec any) (URI, error)

Mint returns the URI that names a record, derived from the type the record was emitted as (which fixes its scheme and authority) and its id field. It is the inverse of Get and the canonical name a data export writes under @id.

func (*Host) Resolve added in v0.2.0

func (h *Host) Resolve(input string) (URI, error)

Resolve turns any input into a canonical URI. It accepts a string that is already a resource URI (canonicalizing its scheme), or an https URL whose host a mounted domain claims (handing it to that domain's Classify). A bare id or @handle is ambiguous without a scheme; use ResolveOn for that.

func (*Host) ResolveOn added in v0.2.0

func (h *Host) ResolveOn(scheme, input string) (URI, error)

ResolveOn turns an input into a URI within a named domain, the home of bare ids and @handles. It reuses the domain's own Classify parser, so it accepts every form that domain's CLI accepts.

func (*Host) Search added in v0.3.3

func (h *Host) Search(ctx context.Context, scheme, query string, limit int) ([]any, error)

Search runs a domain's free-text search op for a query and returns the records it emits, each still the domain's own type (so a caller can Wrap or Mint the hits that are URI-addressable). limit caps the result (0 means the op's own default). It is the query counterpart to Get and List: where those dereference a name, Search discovers names from text.

func (*Host) Searchable added in v0.3.3

func (h *Host) Searchable(scheme string) bool

Searchable reports whether a domain (by scheme or alias) registered a free-text search op, so a host such as ant can decide to offer a search box.

func (*Host) Wrap added in v0.2.0

func (h *Host) Wrap(rec any, fetched time.Time) (Envelope, error)

Wrap builds the Envelope for a record: it mints the record's URI, reads its link fields as URIs, and stamps the fetch time (zero time omits @fetched). Links are grouped by the record field they came from, so a consumer can tell a book's author edge from its similar-books edges.

type HostOption added in v0.2.0

type HostOption func(*hostConfig)

HostOption customizes a Host at Open.

func Tune added in v0.2.0

func Tune(scheme string, fn func(*Config)) HostOption

Tune overlays extra config onto one domain before its client is built, keyed by canonical scheme. It is how a host points a domain at a shared data dir or flips a domain-specific Extra setting without reaching into the driver.

func WithStore added in v0.2.0

func WithStore(st store.Store) HostOption

WithStore tees every dereferenced record into st, so a walk of the graph fills a local store as a side effect, exactly as a single-site read does with --db.

type Identity

type Identity struct {
	Binary  string // binary and command name, e.g. "ccrawl"
	Short   string // one-line description
	Long    string // multi-paragraph description for help
	Version string // semantic version, set by the build
	Site    string // the upstream site or service this CLI targets
	Repo    string // source repository URL
}

Identity is the fixed description of a CLI: its name, what it does, and where it lives. It seeds help text, the API title, the MCP server name, and the default env prefix.

type Input

type Input struct {
	Args    []string       // positional arguments in order
	Flags   map[string]any // named parameters, already typed where possible
	Globals Globals        // resolved framework globals
}

Input is the surface-neutral request a surface hands to Operation.Invoke. The CLI fills Args from positional tokens and Flags from parsed flags; the API fills them from the path and query; MCP fills Flags from the tool arguments object. Globals carries the resolved framework-global flags.

type OpMeta

type OpMeta struct {
	Name    string   // the verb: CLI subcommand, MCP tool suffix, route leaf
	Parent  string   // optional parent command for a nested verb (e.g. "rank" for "rank domain")
	Group   string   // grouping for help (e.g. "read", "data", "meta")
	Summary string   // one line, shown in help, OpenAPI summary, MCP description
	Long    string   // optional multi-line help
	Aliases []string // alternate names
	Args    []Arg    // positional argument schema (names line up with In arg fields)
	Write   bool     // a state-changing op; gated and annotated on every surface
	Single  bool     // emits at most one record (no "no results" on empty stream)

	// URI metadata (8000_uri, 8000_uri_drivers). These make an op addressable by
	// resource URI in a multi-domain host such as ant. They are inert on a
	// single-site binary, so a domain can adopt them with no behavior change.
	URIType  string // the URI authority this op dereferences, e.g. "status" for x's tweet; defaults to the lower-cased Out type name
	Resolver bool   // canonical dereferencer for URIType when several ops share it
	List     bool   // member-lister for a parent resource: `ant ls` / feed:// resolve here
}

OpMeta is the surface-neutral description of one operation. The same metadata drives the CLI subcommand, the HTTP route, and the MCP tool.

type Operation

type Operation interface {
	Meta() OpMeta
	Params() []ParamSpec
	InputSchema() map[string]any
	OutputSchema() map[string]any
	// Invoke binds the surface-neutral Input to the typed In, runs the handler,
	// and routes each emitted record to the sink, applying the store tee and the
	// limit. rt carries the injected handles.
	Invoke(ctx context.Context, in Input, rt RunContext, sink Sink) error
	// OutType is the record type the op emits (struct, pointer stripped), or nil
	// when the op emits no struct. A multi-domain host indexes ops by it so a
	// resource URI's authority can select the op that mints that record type.
	OutType() reflect.Type
}

Operation is what the registry stores and every surface drives. The concrete implementation is the generic op[In, Out] built by Handle.

type Option

type Option func(*App)

Option customizes an App at construction.

func EnvPrefix

func EnvPrefix(prefix string) Option

EnvPrefix overrides the env var prefix (default: upper-cased binary name).

func WithDefaults

func WithDefaults(fn func(*Config)) Option

WithDefaults lets a domain overlay its own config defaults (rate, retries, workers, user agent) onto the framework baseline.

type OutputOptions

type OutputOptions struct {
	Format   string
	Fields   []string
	NoHeader bool
	Template string
	IsTTY    bool
	Color    bool
	Width    int
}

OutputOptions are the resolved rendering settings an escape-hatch command reuses so its structured output matches every operation's: same --output format, --fields projection, --no-header, and --template.

type ParamKind

type ParamKind int

ParamKind classifies how a struct field is filled.

const (
	KindArg    ParamKind = iota // positional argument
	KindFlag                    // named flag / query param / tool argument
	KindInject                  // filled by kit (client, store, cache)
)

type ParamSpec

type ParamSpec struct {
	Name     string
	Kind     ParamKind
	Type     ParamType
	Short    string
	Help     string
	Default  string
	Enum     []string
	Variadic bool
	Inherit  bool // bind to a framework-global flag of the same name
}

ParamSpec is the external description of one bound parameter, used to build CLI flags, query params, and JSON Schema properties.

type ParamType

type ParamType int

ParamType is the wire type of a parameter.

const (
	TypeString ParamType = iota
	TypeInt
	TypeBool
	TypeFloat
	TypeDuration
	TypeStringSlice
)

type Resolver added in v0.2.0

type Resolver interface {
	// Classify turns any accepted input — a bare id, an @handle, a messy https
	// URL — into the canonical (uriType, id). A bare id with no type hint
	// resolves to the domain's default type.
	Classify(input string) (uriType, id string, err error)
	// Locate is the inverse for one resource: the live https location for a
	// (uriType, id).
	Locate(uriType, id string) (url string, err error)
}

Resolver is the optional capability a Domain implements to support `ant resolve` and `ant url`. It is the home of the domain's existing idOrURL parser (Classify) and XxxURL helper (Locate); both are pure string functions, so the URI-native verbs touch no network. A Domain that omits Resolver is still fully addressable for inputs that are already canonical URIs.

type RunContext

type RunContext struct {
	Client any
	Store  store.Store
	Limit  int
}

RunContext carries the injected handles an operation may receive: the domain client, the optional record store, and the resolved limit. A handler reaches these only through KindInject fields, never directly.

type Sink

type Sink interface {
	Emit(rec any) error
	Flush() error
}

Sink is where an operation's records go. Each surface supplies its own: the CLI sink renders to the terminal, the API sink streams an HTTP response, the MCP sink accumulates structured tool content. A handler only ever calls emit(record); swapping the sink swaps the surface with no change to domain code.

type State

type State struct {
	Config  Config
	Globals Globals
	Output  OutputOptions // resolved rendering settings, for escape-hatch commands
	// contains filtered or unexported fields
}

State is the per-run context kit resolves once and shares with every command, whether a registered operation or an escape-hatch command added with AddCommand. It carries the resolved config, the lazily built domain client, and the optional record store. An escape-hatch command reaches it with FromContext(cmd.Context()).

func FromContext

func FromContext(ctx context.Context) *State

FromContext returns the run State stored on the context, or nil if absent. An escape-hatch command calls this to reach the resolved config, client, and store that kit built for the run.

func (*State) Client

func (s *State) Client(ctx context.Context) (any, error)

Client builds the domain client on first use and caches it (and any error) for the rest of the run. It returns nil when the app registered no client factory.

func (*State) Renderer

func (s *State) Renderer(w io.Writer) (*render.Renderer, error)

Renderer builds a render.Renderer over w using the run's resolved output settings. An escape-hatch command that emits records calls this instead of hand-rolling a formatter, then Emit/Flush like any operation sink.

func (*State) Store

func (s *State) Store() store.Store

Store returns the open record store for this run, or nil when --db was not given. Escape-hatch commands that produce records can tee into it themselves.

type URI added in v0.2.0

type URI struct {
	Scheme    string
	Authority string
	Path      []string
	Query     map[string]string
	Fragment  string
}

URI is a parsed resource URI (8000_uri §5.2): a name for a record, not a location. The scheme is a site slug ("goodreads") or a cross-site kind ("host", "pages", "feed", "search", "data"); for a site scheme the authority is the record type and the path is the id, while for host/pages the authority is a real hostname.

func ParseURI added in v0.2.0

func ParseURI(s string) (URI, error)

ParseURI parses a canonical-shaped resource URI. It is deliberately strict: it does not normalize messy https URLs or bare ids (that is Host.Resolve's job, which reuses a domain's parser); it only reads an already-URI-shaped string into its parts. The scheme and authority are lower-cased; path segments keep their case, since some domains have case-sensitive ids.

func (URI) DataPath added in v0.2.0

func (u URI) DataPath() string

DataPath is the on-disk path for this URI under the data root, without an extension: <scheme>/<authority>/<id...>. It is the single rule of 8000_uri §6.1 — the file path is the URI — so a tree of these is self-describing.

func (URI) ID added in v0.2.0

func (u URI) ID() string

ID is the path joined with "/", the record's id (with any sub-ids). It is the arg a resolver op receives.

func (URI) String added in v0.2.0

func (u URI) String() string

String renders the canonical serialization, the inverse of ParseURI. The query keys are sorted so equal URIs serialize byte-for-byte equal (8000_uri §3.2).

Directories

Path Synopsis
Package errs defines the typed error taxonomy shared by every kit-based CLI and its one true mapping to CLI exit codes, HTTP status codes, and MCP error objects.
Package errs defines the typed error taxonomy shared by every kit-based CLI and its one true mapping to CLI exit codes, HTTP status codes, and MCP error objects.
Package render turns a stream of record structs into one of the output formats the kit CLI supports: table, markdown, json, jsonl, csv, tsv, url, raw, and a Go text/template.
Package render turns a stream of record structs into one of the output formats the kit CLI supports: table, markdown, json, jsonl, csv, tsv, url, raw, and a Go text/template.
Package store defines the pluggable record-store SPI that lets any kit read double as a crawl: with --db set, every emitted record is upserted into a local store before it reaches the output.
Package store defines the pluggable record-store SPI that lets any kit read double as a crawl: with --db set, every emitted record is upserted into a local store before it reaches the output.
sqlitestore
Package sqlitestore is the built-in default store backend.
Package sqlitestore is the built-in default store backend.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL