# nanobot Ultra-lightweight personal AI assistant. TypeScript port of [nanobot](https://github.com/HKUDS/nanobot), inspired by [Openclaw](https://github.com/openclaw/openclaw). Runs as a chat-controlled bot (`gateway`) with pluggable "channels" for different services, or as a local interactive CLI (`agent`). Uses the [Vercel AI SDK](https://sdk.vercel.ai) for LLM abstraction, so it works with Anthropic, OpenAI, Google, OpenRouter, and Ollama out of the box. ## Requirements - [Bun](https://bun.sh) ≥ 1.0 ## Install ```bash git clone cd nanobot-ts bun install # or use `mise install` ``` ## Quick start **1. Create a config file** ```bash mkdir -p ~/.nanobot ``` `~/.nanobot/config.json`: ```json { "providers": { "openrouter": { "apiKey": "sk-or-v1-..." } }, "agent": { "model": "openrouter/anthropic/claude-sonnet-4-5" } } ``` **2. Chat** ```bash bun run start agent ``` That's it. ## Commands ### `agent` — local CLI Chat with the agent from your terminal. Does not require a running gateway. ``` bun run start agent [options] ``` | Option | Description | |--------|-------------| | `-c, --config ` | Path to `config.json` (default: `~/.nanobot/config.json`) | | `-m, --message ` | Send a single message and exit (non-interactive) | | `-w, --workspace ` | Override the workspace directory | | `-M, --model ` | Override the model for this session | **Interactive mode** (default when no `-m` is given): ```bash bun run start agent bun run start agent -c ~/.nanobot-work/config.json bun run start agent -w /tmp/scratch ``` Press `Ctrl+C` to exit. **Single-shot mode:** ```bash bun run start agent -m "What time is it in Tokyo?" bun run start agent -m "Summarize the file ./notes.md" ``` ### `gateway` — Mattermost bot Runs the full stack: Mattermost WebSocket channel, agent loop, cron scheduler, and heartbeat. ``` bun run start gateway [options] ``` | Option | Description | |--------|-------------| | `-c, --config ` | Path to `config.json` (default: `~/.nanobot/config.json`) | ```bash bun run start gateway bun run start gateway -c ~/.nanobot-work/config.json ``` Handles `SIGINT` / `SIGTERM` for graceful shutdown. ## Configuration Config file: `~/.nanobot/config.json` (or pass `-c ` to any command). Environment variable overrides: | Variable | Config equivalent | |----------|-------------------| | `NANOBOT_CONFIG` | path to config file | | `NANOBOT_MODEL` | `agent.model` | | `NANOBOT_WORKSPACE` | `agent.workspacePath` | ### Full config reference ```json { "agent": { "model": "openrouter/anthropic/claude-sonnet-4-5", "workspacePath": "~/.nanobot", "maxTokens": 4096, "contextWindowTokens": 65536, "temperature": 0.7, "maxToolIterations": 40 }, "providers": { "anthropic": { "apiKey": "..." }, "openai": { "apiKey": "..." }, "google": { "apiKey": "..." }, "openrouter": { "apiKey": "..." }, "ollama": { "apiBase": "http://localhost:11434/api" } }, "channels": { "sendProgress": true, "sendToolHints": true, "mattermost": { "serverUrl": "mattermost.example.com", "token": "YOUR_BOT_TOKEN", "scheme": "https", "port": 443, "allowFrom": ["your-user-id"], "groupPolicy": "mention", "groupAllowFrom": [], "dm": { "enabled": true, "allowFrom": [] }, "replyInThread": true } }, "tools": { "restrictToWorkspace": false, "exec": { "timeout": 120, "denyPatterns": [], "restrictToWorkspace": false, "pathAppend": "" }, "web": { "braveApiKey": "BSA...", "proxy": "http://proxy:8080" } }, "heartbeat": { "enabled": false, "intervalMinutes": 30 } } ``` ### Providers Model names use a `provider/model` prefix scheme: | Prefix | Provider | Example | |--------|----------|---------| | `anthropic/` | Anthropic direct | `anthropic/claude-opus-4-5` | | `openai/` | OpenAI direct | `openai/gpt-4o` | | `google/` | Google direct | `google/gemini-2.5-pro` | | `openrouter/` | OpenRouter (any model) | `openrouter/anthropic/claude-sonnet-4-5` | | `ollama/` | Local Ollama | `ollama/llama3.2` | For Ollama, set `providers.ollama.apiBase` (default: `http://localhost:11434/api`). ### Mattermost setup 1. In Mattermost: **Main Menu → Integrations → Bot Accounts → Add Bot Account** 2. Enable `post:all` permission, copy the access token 3. Add to config: ```json { "channels": { "mattermost": { "serverUrl": "mattermost.example.com", "token": "YOUR_BOT_TOKEN", "allowFrom": ["your-mattermost-user-id"] } } } ``` 4. Run `bun run start gateway` `allowFrom` controls which users the bot responds to. Use `["*"]` to allow all users. `groupPolicy` controls group channel behaviour: - `"mention"` (default) — only respond when @mentioned - `"open"` — respond to all messages - `"allowlist"` — restrict to users in `groupAllowFrom` ### Security | Option | Default | Description | |--------|---------|-------------| | `tools.restrictToWorkspace` | `false` | Restrict file and shell tools to the workspace directory | | `tools.exec.denyPatterns` | `[]` | Additional shell command patterns to block | | `tools.exec.pathAppend` | `""` | Extra directories appended to `PATH` for shell commands | | `channels.mattermost.allowFrom` | `[]` | User ID allowlist. Empty denies everyone; `["*"]` allows all | ### Heartbeat When `heartbeat.enabled` is `true`, the gateway wakes up every `intervalMinutes` and checks `HEARTBEAT.md` in the workspace. If the file lists tasks, the agent runs them and can deliver results via the `message` tool. ```markdown ## Periodic Tasks - [ ] Check the weather and send a summary - [ ] Review open GitHub issues ``` ## Multiple instances Run separate instances with different configs — useful for isolated workspaces, different models, or separate teams. ```bash # Instance A bun run start gateway -c ~/.nanobot-a/config.json # Instance B bun run start gateway -c ~/.nanobot-b/config.json ``` Each instance needs its own config file. Set a different `agent.workspacePath` per instance to keep memory, sessions, and cron jobs isolated: ```json { "agent": { "workspacePath": "~/.nanobot-a" } } ``` To run a local CLI session against a specific instance: ```bash bun run start agent -c ~/.nanobot-a/config.json -m "Hello" # Temporarily override the workspace for a one-off run bun run start agent -c ~/.nanobot-a/config.json -w /tmp/scratch ``` ## Linux service (systemd) ```ini # ~/.config/systemd/user/nanobot-gateway.service [Unit] Description=Nanobot Gateway After=network.target [Service] Type=simple ExecStart=/path/to/bun run --cwd /path/to/nanobot-ts start Restart=always RestartSec=10 [Install] WantedBy=default.target ``` ```bash systemctl --user daemon-reload systemctl --user enable --now nanobot-gateway journalctl --user -u nanobot-gateway -f ``` To keep the service running after logout: `loginctl enable-linger $USER` ## Project structure ``` nanobot-ts/ ├── index.ts # Entry point ├── src/ │ ├── agent/ │ │ ├── loop.ts # Agent loop (LLM ↔ tool execution) │ │ ├── context.ts # System prompt builder │ │ ├── memory.ts # Long-term memory + consolidation │ │ ├── skills.ts # Skills loader │ │ ├── subagent.ts # Background task execution │ │ └── tools/ │ │ ├── base.ts # Tool interface + ToolRegistry │ │ ├── filesystem.ts # read_file, write_file, edit_file, list_dir │ │ ├── shell.ts # exec │ │ ├── web.ts # web_search (Brave), web_fetch │ │ ├── message.ts # message (send to chat) │ │ ├── spawn.ts # spawn (background subagent) │ │ └── cron.ts # cron (schedule management) │ ├── channels/ │ │ ├── base.ts # BaseChannel interface │ │ ├── mattermost.ts # Mattermost (raw WebSocket + fetch) │ │ └── manager.ts # Channel lifecycle + outbound routing │ ├── bus/ │ │ ├── types.ts # InboundMessage, OutboundMessage schemas │ │ └── queue.ts # AsyncQueue, MessageBus │ ├── provider/ │ │ ├── types.ts # LLMResponse, ToolCall, ChatOptions │ │ └── index.ts # LLMProvider (Vercel AI SDK wrapper) │ ├── session/ │ │ ├── types.ts # SessionMessage, SessionMeta schemas │ │ └── manager.ts # Session persistence (JSONL) │ ├── cron/ │ │ ├── types.ts # CronJob, CronSchedule schemas │ │ └── service.ts # CronService (at / every / cron expr) │ ├── heartbeat/ │ │ └── service.ts # HeartbeatService │ ├── config/ │ │ ├── types.ts # Zod config schemas │ │ └── loader.ts # loadConfig, env overrides │ └── cli/ │ └── commands.ts # gateway + agent commands ├── templates/ # Default workspace files (SOUL.md, etc.) └── skills/ # Bundled skills (weather, github, etc.) ```