Documentation
¶
Overview ¶
Package app is nib's entrypoint, importable so nib can be embedded in another binary. nib's own main.go is a thin wrapper over Main.
Index ¶
Constants ¶
const ExitCodeApprovalNoInput = 3
ExitCodeApprovalNoInput is the exit code for a CLI session that refused a tool call because stdin was closed and nothing could approve it. See cmd.ErrApprovalNoInput for why that is not a success.
It is deliberately neither 0 nor 1: a script piping a prompt in has to be able to tell "answered" from "refused to act" without reading stdout, and from a crash or a bad flag (2) without guessing. Scripts branch on this number, so it is part of nib's interface and does not change.
Variables ¶
This section is empty.
Functions ¶
func Main ¶
Main runs nib and returns a process exit code. argv is the full argument vector including the program name. It routes through the same code as Run, so the process-wide xlog side effect noted there applies here too.
func Run ¶
Run runs nib and returns an error. It never calls os.Exit.
Every failure comes back as a bare ExitError carrying nothing but an exit code, the cause having been written to Stderr instead: an unparseable flag, an unknown --init shell, an MCP transport failure, the setup abort, the injected-stream refusal and a management subcommand's non-zero code are all indistinguishable to the caller. The code is 1 for every one of those except two: an unparseable flag is 2, and a CLI session that had to refuse a tool call because stdin could not approve it is ExitCodeApprovalNoInput.
Cancelling ctx unwinds whichever mode is running, the TUI included, and comes back as ExitError{1} with the context's error on Stderr. Signals are the caller's: this entrypoint installs no handler, and nothing below it listens for one either.
Run also reconfigures the process-wide xlog logger from the resolved config, which an embedder sharing that logger will observe.
Types ¶
type ExitError ¶
type ExitError struct{ Code int }
ExitError carries a non-zero exit code out of Run.
type Options ¶
type Options struct {
// Args are the arguments after the program name (os.Args[1:]).
Args []string
// ProgramName is the name shown in the messages this package prints itself:
// the --version line, the flag package's usage and parse errors, the setup
// gate's two aborts, and the injected-stream refusal. Empty means "nib".
//
// It also renames the --init shell snippets, which is more than a cosmetic
// substitution: the widget they define invokes this name, so an embedder's
// Ctrl+Space runs the embedder's command rather than a `nib` its users do
// not have. A name of several words ("local-ai chat") stays several words
// in command position, and the widget's function name is derived from it by
// reducing it to an identifier.
//
// The rule for everything below is one line: if a string names a command
// that someone is expected to RUN, this renames it; if it merely names the
// tool, it does not.
//
// Renamed, beyond the messages and scripts above:
//
// - The tmux split. Inside tmux the Ctrl+Space widget lands in a split
// pane, and the pane has to re-enter THIS program. That is the host
// binary plus its subcommand, not the executable path alone, so an
// embedded nib without this spawns the host's default command instead.
// - The management subcommands' instructions: the `usage: ...` lines of
// `plugin`, `skill` and `mcp`, and the hints that follow an install
// ("Enable later: local-ai chat plugin enable foo", "verify now with:
// local-ai chat mcp test bar"). The enable hint is reached on every
// non-interactive `plugin install` without --yes.
// - The skill installer's suggestions, which reach the user through those
// same subcommands: "use `local-ai chat skill update x`" on a duplicate
// pack, and "did you mean `local-ai chat plugin install`?" on a source
// with no SKILL.md. The catalog install path included.
// - The SYSTEM PROMPT paragraph about registering MCP servers. This is the
// one the model relays: it paraphrases the advice in its own words at
// whatever moment seems relevant, and unlike a usage line the user gets
// no cue that the tool is called something else here. It travels via
// types.Config.ProgramName, which this field populates during the load
// and always wins over a value arriving through Defaults or Overrides.
//
// NOT renamed, deliberately, so these still say "nib" whatever this is set
// to:
//
// - The CLI and TUI branding (theme.BrandName, the banner, the setup
// wizard's header). It names the tool; it is not a command.
// - Prose in terminal output that names the tool rather than instructing,
// specifically the "available on the next nib session" clause of the mcp
// add confirmation, whose "verify now with:" half IS renamed. The
// surrounding output makes the referent obvious; the system prompt,
// which has no such context, renames the equivalent sentence.
// - The "nib: ..." diagnostics that config loading and plugin/skill
// discovery write straight to os.Stderr on every run.
// - Internal identifiers that are never shown or typed: the MCP server's
// implementation name and its nib/reply notification method, the
// nib-plugin.yaml manifest filename, the ~/.config/nib state root, and
// the tmux temp-file prefix and wait-for channel.
ProgramName string
// BaseDir overrides the config, plugins, and skills root. Empty means
// nib's default XDG resolution.
BaseDir string
// Stdin, Stdout, Stderr default to the process streams when nil.
//
// CLI mode (--cli) reads and writes exactly these. The TUI, which is the
// DEFAULT mode, does not: it renders on /dev/tty, because the terminal is
// still there even when stdout is a pipe, and serving that case is why it
// opens /dev/tty at all. Only the shell-capture line the TUI prints on exit
// goes to Stdout.
//
// For an embedder that is a rule rather than a caveat: injecting a Stdin or
// a Stdout that is NOT a terminal (a buffer, a pipe, a file) requires --cli,
// or the run is refused with an error saying so, rather than rendering into
// a stream the TUI cannot drive. Injecting the process streams, or any other
// terminal, leaves every mode working.
//
// Only those two are gated (see decideStreamRefusal). A non-terminal Stderr
// is always accepted, because every error this package prints goes through
// o.stderr() rather than through the TUI, so a TUI session with its error
// output captured to a buffer or a log file is a supported combination.
//
// One consequence is worth spelling out, because it inverts the usual
// instinct that passing os.Stdout is the safe, explicit choice. An embedder
// that wants nib's shell-capture idiom, where the user runs
// `out=$(myprog agent)` and the TUI prints the selected command on stdout
// for the shell to capture, must leave Stdout NIL rather than set it to
// os.Stdout. Nil means "not injected": nib falls back to the process stream,
// the TUI runs, and the capture line lands on stdout as it should. Setting
// os.Stdout explicitly injects whatever stdout happens to be, and under
// $(...) that is a pipe, so the run is refused for a stream nib would have
// used anyway.
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
// Defaults seed config fields the config file leaves empty. Every field of
// types.Config is seedable except BaseDir, which is ignored: the root has
// exactly one knob, the BaseDir field above, and it overwrites any seeded
// value. See config.LoadOptions.Defaults for what "empty" means for maps,
// slices and booleans, including that a seeded true beats an explicit
// `false:` in the config file for every bool.
Defaults types.Config
// Overrides are the other half of Defaults: they sit ABOVE the config file
// and above the bare environment variables, so an embedder can hand nib a
// value the user's config file must not undo. A CLI flag is the case this
// exists for. Routed through Defaults, `--endpoint` is accepted and then
// discarded whenever the file already carries a base_url, which is the
// normal state rather than an edge case, since anything that writes the file
// once makes the flag dead from the next run on.
//
// Every field of types.Config is overridable except BaseDir, ignored for the
// same reason it is ignored in Defaults. See config.LoadOptions.Overrides for
// the merge rules, and in particular for the one asymmetry: an override can
// only raise a field, never blank one, so an override of false CANNOT beat a
// `true:` in the config file.
//
// Two fields stay above it, by design rather than by omission. nib's own
// --trace-dir and --yolo, and their NIB_TRACE_DIR and NIB_YOLO twins, are
// resolved after the config load and still win for TraceDir and
// ApprovalMode. Both are deliberate instructions to nib rather than ambient
// environment, which is what separates them from the bare MODEL / API_KEY /
// BASE_URL that SkipBareEnv exists to suppress.
//
// One consequence is sharp and worth stating outright: NIB_YOLO=1 ESCALATES
// PAST A STRICTER OVERRIDE. An embedder that sets Overrides.ApprovalMode to
// "strict", "allowlist" or "prompt" is silently downgraded to "auto", which
// approves every tool call. Filtering Args does not help, because this
// arrives through the environment: an embedder that must prevent it controls
// the child process environment, i.e. does not pass NIB_YOLO through.
//
// TraceDir has no equivalent hazard: the config file cannot set it at all
// (it is runtime-only), so routing --trace-dir through Defaults already
// worked and NIB_TRACE_DIR merely retargets where a trace is written.
Overrides types.Config
// SkipSetup suppresses the first-run model wizard. Embedders that resolve
// the model themselves set this. It suppresses the whole gate, so an
// explicit --setup in Args becomes a silent no-op rather than an error.
SkipSetup bool
// SkipBareEnv suppresses the bare MODEL / API_KEY / BASE_URL variables, and
// only those three. nib's prefixed variables keep reading the ambient
// environment and still outrank a seed: NIB_TRACE_DIR overrides a seeded
// TraceDir, NIB_YOLO forces ApprovalMode to "auto" over a seeded one, and
// LOG_FORMAT selects the log encoding.
SkipBareEnv bool
}
Options configures a nib invocation. The zero value reproduces standalone nib's behavior exactly, so embedders opt in to each difference.