@saluzi/min
v1.2.1
Published
Min Agent: minimum agent for edge devices
Readme
min-agent
A minimal coding agent for edge devices. TypeScript, ESM, runs on Bun or Node.
@saluzi/min implements the smallest usable coding-agent core: a reasoning
loop, a handful of file/shell tools, opt-in context-window compaction, path-safe
permissions, a model/provider layer, and a plugin system that makes tools,
hooks, prompts, skills, and compaction replaceable or extendable — all without
hot-loading.
Install
bun installBuild
bun run build # tsc → dist/CLI
bun run dist/cli.js [options] [message]Or after bun link / global install:
min-agent [options] [message]
mina [options] [message]Options:
| Option | Description | Default |
| --- | --- | --- |
| --model <spec> | Model spec [provider:]model-id (see Models) | resolved (see Models) |
| --base-url <url> | Override the API base URL for this run (writes the provider's dedicated <PROVIDER>_BASE_URL) | — |
| --cwd <path> | Working directory | current directory |
| --project-root <path> | Project root (for path safety) | cwd |
| --max-turns <n> | Max LLM iterations per user turn | 100 |
| --permission <mode> | default | acceptWrite | bypass | default |
| --skills-dir <path> | Directory to scan for SKILL.md files | resolved (see Skills) |
| --plugins-dir <path> | Directory to scan for file plugins (plugin.json) | resolved (see Plugins) |
| --trust-plugin <name> | Trust a named file plugin's code for this run (repeatable) | — |
| --trust-plugins | Trust all file plugins for this run (automation) | — |
| --compact | Enable context compaction (see Compaction) | off |
| --compact=<path> | Enable compaction with an external strategy module | — |
| --debug | Enable debug logging to ~/.min/debug.log | — |
| --help | Show help | — |
Pass a positional message for single-shot mode; omit it for interactive mode.
Interactive mode
- Model banner — startup prints
▶ model <id> · provider <p>so a misrouted model is visible immediately (single-shot mode stays quiet). - Turn counter — a turn is one user input. The
─── Turn N ───banner appears once per message you send and grows across the session; tool calls and extra LLM round-trips within a run do not advance it. - Multi-line paste — pasting multi-line text inserts a compact tag into
the input line, e.g.
You: summarize this [Pasted text #1 +12 lines]. The full pasted content is stored and sent to the model when you press Enter (single-line pastes are inserted verbatim). Works via bracketed-paste mode on modern terminals, with a timing fallback elsewhere. - Ctrl+C — aborts the current run; press twice at the prompt to exit. During an approval prompt it denies the tool call and aborts.
Examples
# OpenAI — zero flags: the single provider with credentials is picked up
OPENAI_API_KEY=sk-... OPENAI_MODEL=gpt-4o min-agent
# Claude via Anthropic proxy
ANTHROPIC_BASE_URL=https://my-proxy.com ANTHROPIC_AUTH_TOKEN=sk-... \
min-agent --model claude-sonnet-4-5
# Local Ollama model (OpenAI-compatible) with a small context window.
# Model ids containing a colon need the openai_compat: prefix.
OPENAI_COMPATIBLE_BASE_URL=http://localhost:11434/v1 \
OPENAI_COMPATIBLE_API_KEY=ollama MIN_MAX_CONTEXT_TOKENS=32768 \
OPENAI_MAX_TOKENS=4096 min-agent --model openai_compat:qwen2.5-coder:7b
# DeepSeek (OpenAI-compatible endpoint; the base URL is required)
OPENAI_COMPATIBLE_API_KEY=sk-... \
OPENAI_COMPATIBLE_BASE_URL=https://api.deepseek.com/v1 \
min-agent --model deepseek-chat
# Enable auto-compaction, triggering at 50% of the context window
MIN_AUTOCOMPACT_PCT=50 min-agent --compact --model gpt-4o
# Enable compaction with an external strategy module
min-agent --compact=./my-compact.mjs --model gpt-4o
# Bypass permissions (sandboxed/automation only)
min-agent --permission bypass --model gpt-4o
# Load file plugins and trust a code plugin
min-agent --trust-plugin web-tools --model gpt-4oEnvironment variables
Auth
| Var | Provider |
| --- | --- |
| OPENAI_API_KEY | openai |
| ANTHROPIC_API_KEY | anthropic |
| ANTHROPIC_AUTH_TOKEN | anthropic proxies (sent as Bearer) |
| OPENAI_COMPATIBLE_API_KEY | openai_compatible |
Model selection (when --model is omitted)
| Var | Effect |
| --- | --- |
| MIN_DEFAULT_MODEL | Default model spec, same syntax as --model (highest priority) |
| OPENAI_MODEL | Model id for the openai provider (requires OPENAI_API_KEY) |
| ANTHROPIC_MODEL | Model id for the anthropic provider (requires ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN) |
| OPENAI_COMPATIBLE_MODEL | Model id for the openai_compatible provider (requires OPENAI_COMPATIBLE_API_KEY) |
When --model is omitted, the agent picks the single builtin provider
that has credentials and uses its <PROVIDER>_MODEL. Zero or multiple
providers with credentials → an actionable startup error listing the three
configuration ways. Fail loud beats guessing.
Base URL
| Var | Effect |
| --- | --- |
| OPENAI_BASE_URL | Redirects the openai provider |
| ANTHROPIC_BASE_URL | Redirects the anthropic provider |
| OPENAI_COMPATIBLE_BASE_URL | Redirects the openai_compatible provider only |
| <PROVIDER>_BASE_URL | Redirects a plugin-registered provider |
Legacy: OPENAI_BASE_URL still redirects openai_compatible when
OPENAI_COMPATIBLE_BASE_URL is unset. --base-url always writes the
dedicated variable, so redirecting a compat provider never hijacks the
official openai endpoint.
Debug
| Var | Effect |
| --- | --- |
| MIN_DEBUG | 1 / true / verbose enables debug logging |
| MIN_DEBUG_FILE | Override log file path (default ~/.min/debug.log) |
| MIN_DEBUG=stderr | Mirror logs to stderr in human-readable form |
Context window & compaction (compaction is OFF by default — enable with
--compact / --compact=<path>; the compaction vars below only take effect
once it is on)
| Var | Effect |
| --- | --- |
| MIN_MAX_CONTEXT_TOKENS | Override the effective context window (any model). Highest priority. |
| MIN_AUTO_COMPACT_WINDOW | Secondary context window override, for local/custom models. |
| OPENAI_MAX_TOKENS | Override max output tokens for openai + openai_compatible. |
| MIN_MAX_OUTPUT_TOKENS | Generic max output tokens override (any provider). |
| MIN_AUTOCOMPACT_PCT | Percentage threshold (0–100) to trigger autocompact. Only makes it more aggressive. |
| MIN_KEEP_RECENT_TOKENS | Override keep-recent token budget (default 20000). |
| MIN_RESERVE_TOKENS | Override reserve token budget (default 16384). |
| DISABLE_COMPACT | Disable compaction entirely — even when --compact is passed. |
| DISABLE_AUTO_COMPACT | Disable auto-compact only. |
Models
A model spec is [provider:]model-id, split at the first colon. There is
no per-model builtin table — snapshot-dated ids rot, and aliases silently
mapped to stale models. Resolution order:
- Explicit provider prefix —
anthropic:claude-sonnet-4-5,openai_compat:qwen2.5-coder:7b(openai_compatis an input alias foropenai_compatible; the rest of the string is the model id, colons included). The provider must be registered (builtin or plugin) — an unknown prefix fails at startup with the registered-provider list instead of silently routing your API key to a fallback host. - Plugin-registered models — a bare id matching some provider's
models: [...]entry (see Custom providers). - Brand inference (prefix-only, never version-aware):
claude*→ anthropic,gpt*/chatgpt*/o<digit>*→ openai. - Fallback — any other bare id resolves to
openai_compatible, so local models work by id:--model qwen2.5-coderwithOPENAI_COMPATIBLE_BASE_URLpointed at Ollama/vLLM.
Bare ids that contain a colon (Ollama-style qwen2.5-coder:7b) must be
prefixed explicitly (openai_compat:qwen2.5-coder:7b) — a colon always means
"provider prefix".
When --model is omitted, the default is MIN_DEFAULT_MODEL or the single
builtin provider with credentials (<PROVIDER>_MODEL + key, see
Environment variables).
Context window / max output tokens default to 128k / 16k for every model;
override via MIN_MAX_CONTEXT_TOKENS / OPENAI_MAX_TOKENS /
MIN_MAX_OUTPUT_TOKENS (see the env tables), or declare exact values on a
plugin provider's model entries.
Migration (v1.2)
- Aliases removed —
claude,sonnet,gpt4,deepseekno longer map to fixed snapshots. Use full ids (claude-sonnet-4-5) or specs. - Colon-containing ids need a prefix —
--model qwen2.5-coder:7b→--model openai_compat:qwen2.5-coder:7b. - DeepSeek is no longer a builtin endpoint — set
OPENAI_COMPATIBLE_BASE_URL=https://api.deepseek.com/v1(see the example above). - Metadata changes — claude models previously carried a 200k context
window from the builtin table; they now use the 128k default (auto-compact
triggers earlier). Override with
MIN_MAX_CONTEXT_TOKENSif you rely on the larger window. - Multiple keys — with credentials for 2+ providers set, the agent used
to silently default to
gpt-4o; it now fails at startup and asks you to pick explicitly.
Programmatic API
import { MinAgent } from "@saluzi/min";
const agent = new MinAgent({
// model is optional: omitted → MIN_DEFAULT_MODEL, or the single builtin
// provider with credentials. String specs use the [provider:]model-id
// syntax; a prebuilt ModelConfig object also works.
model: "gpt-4o",
cwd: process.cwd(),
permissionMode: "default", // requires askUser for default/acceptWrite
askUser: async (toolCall, reason) => true,
// plugins: [...], // configuration-time only
// skillsDir: "./.min/skills",
// compaction: true, // enable auto-compaction (off by default);
// // or "./my-compact.mjs" for an external strategy
});
const result = await agent.run("Add a vitest test for src/utils/env.ts");
console.log(result.totalTurns, result.stoppedReason);
agent.abort(); // stop the current run at the next safe point
agent.installSigintHandler(); // Ctrl+C → abortKey exports: MinAgent, createMinAgent, resolveModel, createDefaultTools
(and each tool factory), MinimalSession, plus the plugin/skills helpers shown
under Plugins and Skills.
Tools
Built-in tools: bash, read, write, edit, grep, ls, find.
- Concurrent writes to the same file are serialized automatically.
- File operations are confined to the working/project root (no traversal outside it).
- Tool output is truncated to protect the context window.
readshows at most the first 2000 lines (or first 50KB) of a file and includes the nextstartLineto continue from; files above 5MB are refused.
Permissions
Three modes:
default— ask beforebash/write/edit(requiresaskUser).acceptWrite— writes allowed, still asks for shell.bypass— no prompts (sandbox/automation only).
Approval is requested through your askUser callback; output is also guarded
against context overflow.
Compaction
Compaction is a replaceable plugin and is off by default — the core
loop never compacts unless a strategy is registered. Enable the builtin
auto-compaction with --compact (or compaction: true in the programmatic
API): when the context approaches the window limit, the loop finds a cut
point, summarizes the dropped messages through the main model, and keeps a
recent tail. Thresholds are tuned by the env vars above; DISABLE_COMPACT=1
is a kill switch that wins over the flag.
Point at an external compact with --compact=<path> (or
compaction: "./my-compact.mjs"). The module's default export is either a
CompactionStrategy object or a bare async function; both receive the current
entries, model config, and stream function, and return a CompactionResult
(or null when nothing should be compacted):
// my-compact.mjs — an external compaction strategy
export default async (entries, model, streamFn) => {
// …your own threshold / cut-point / summarizer logic…
return {
type: "compaction",
summary: "…",
tokensBefore: 12345,
retainedTail: [/* AgentMessage[] kept after the cut */],
};
};Any plugin can also replace compaction at setup time via
host.setCompactionStrategy(...) (last-writer-wins), and tune it with
host.setCompactionSettings(...) — including the "compaction" field of a
plugin.json manifest. A failed external-module load warns and leaves
compaction off (it never silently falls back to the builtin summarizer).
Plugins
A plugin runs at construction time and may replace or add capabilities — no
hot-loading. Through the PluginHost a plugin can:
- register/replace tools
- add
beforeToolCall/afterToolCallhooks - add prompt sections, guidelines, context files, or replace the prompt builder
- replace the stream function, session storage, or compaction strategy
- add context transforms, steering/follow-up message sources
- register custom model providers (
host.registerProvider) - publish capabilities on a
CapabilityBus(consumed by other plugins)
Programmatic plugins
import { MinAgent, type MinPlugin } from "@saluzi/min";
const myPlugin: MinPlugin = {
name: "style",
setup(host) {
host.addTool(myTool);
host.beforeToolCall(myGuard);
host.addPromptSection({ heading: "Style", body: "Prefer named exports." });
},
};
new MinAgent({ model: "gpt-4o", plugins: [myPlugin] });File plugins
Drop a plugin.json manifest into .min/plugins/<name>/ (project-local) or
~/.min/plugins/<name>/ (user-global). A manifest can declare tools,
providers, prompt sections, guidelines, and more — declaratively (no code) or
with code modules (tools[].module, module, provider.streamModule).
.min/plugins/
web-tools/
plugin.json # manifest: tool declarations + refs
web_search.mjs # tool execute implementation{
"name": "web-tools",
"tools": {
"items": [
{
"name": "web_search",
"description": "Search the web.",
"parameters": { "type": "object", "properties": { "q": { "type": "string" } }, "required": ["q"] },
"module": "./web_search.mjs"
}
]
}
}// web_search.mjs — export default a ToolExecutor
export default async (args, ctx) => {
ctx.onUpdate?.("searching...");
return { content: "results...", isError: false };
};Code modules require trust (--trust-plugin <name> or trust.json);
untrusted plugins apply only their declarative fields — with one exception:
the provider field also requires trust (it redirects LLM traffic and
attaches credentials). See ~/.min/plugins/plugins.md for the full guide
(auto-generated on first run).
Custom providers
Register a provider via host.registerProvider(def) or the provider field
in plugin.json. api: "openai" | "anthropic" reuses the builtin protocol;
streamModule gives a fully custom stream handler. Security rules: provider
registration requires trust; apiKeyEnv/authTokenEnv must stay in the
provider's namespace (<PROVIDER>_API_KEY / <PROVIDER>_AUTH_TOKEN); builtin
provider names (openai / anthropic / openai_compatible) are reserved —
redirect them with their dedicated *_BASE_URL vars or --base-url.
{
"name": "gemini-bridge",
"provider": {
"name": "gemini",
"api": "openai",
"baseUrl": "https://generativelanguage.googleapis.com/v1",
"apiKeyEnv": "GEMINI_API_KEY",
"models": [{ "modelId": "gemini-1.5-pro", "contextWindow": 1000000, "maxOutputTokens": 8192 }]
}
}Then min-agent --model gemini-1.5-pro resolves via the provider's model
list. ModelConfig.provider is a plain string, so any provider name works.
Skills
Agent-Skills convention: each skill is a SKILL.md file with YAML frontmatter
(name, description, optional disable-model-invocation). Only frontmatter is
loaded — the body is read on demand by the LLM via the read tool (lazy
loading). Skills are injected as a pure plugin that adds a catalog section to
the system prompt.
Resolution (first non-empty existing directory wins):
--skills-dir/MinAgentConfig.skillsDirMIN_SKILLS_DIRenv var<cwd>/.min/skills/(project-local)~/.min/skills/(user-global)
Selecting / disabling skills (allowlist / denylist)
An optional skills.config.json in the skills directory selects or excludes
skills by their frontmatter name (mirrors workflows.config.json):
{ "enabled": ["plan", "review"], "disabled": ["experimental"] }--skill <name> (repeatable) / MinAgentConfig.skills sets an explicit
allowlist that overrides enabled — only the named skills load:
min-agent --skill plan --skill review --model gpt-4omkdir -p .min/skills
cat > .min/skills/release.md <<'EOF'
---
name: release
description: Cut a new release bump and tag.
---
1. Run `bun run build`.
2. Bump version in package.json.
3. Commit and tag.
EOFScripts
bun run build # tsc
bun run test # vitest run
bun run test:watch # vitest
bun run typecheck # tsc --noEmit
bun run start # tsx src/index.tsLicense
MIT
