app

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2026 License: MIT Imports: 18 Imported by: 0

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

View Source
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

func Main(argv []string) int

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

func Run(ctx context.Context, o Options) error

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.

func (ExitError) Error

func (e ExitError) Error() string

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.

Jump to

Keyboard shortcuts

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