nanoagent is a small TypeScript coding agent built as a learning exercise. Milestone 10 adds a Pi-inspired native TUI on top of the existing runtime, stdio MCP support, slash commands, layered settings, and CLAUDE.md-style memory loading:
- CLI
- TUI
- JSON-RPC over stdin/stdout
- Node.js 22+
pnpmvia Corepack or a local install- Python 3 for PTY-backed TUI integration tests
corepack enable
corepack prepare [email protected] --activate
pnpm installCreate a .env file from .env.example and set ANTHROPIC_API_KEY before using the chat paths.
You can also copy nanoagent.policy.example.json to nanoagent.policy.json and adjust tool permission defaults or overrides.
You can copy nanoagent.hooks.example.json to nanoagent.hooks.json to enable sample hooks.
You can copy nanoagent.settings.example.json to nanoagent.settings.json and CLAUDE.example.md to CLAUDE.md to start using layered settings and project memory.
The same settings file can also define custom slash commands.
pnpm test
pnpm typecheck
pnpm nanoagent --help
pnpm nanoagent config
pnpm nanoagent chat "Explain this repository"
pnpm nanoagent chat --session "<session-id>" "Continue from the last exchange"
pnpm nanoagent --session "<session-id>" "Continue from the last exchange"
pnpm nanoagent-tui
echo '{"jsonrpc":"2.0","id":1,"method":"runtime.info"}' | pnpm nanoagent-rpc
echo '{"jsonrpc":"2.0","id":1,"method":"config.inspect"}' | pnpm nanoagent-rpc
echo '{"jsonrpc":"2.0","id":1,"method":"tools.list"}' | pnpm nanoagent-rpc
echo '{"jsonrpc":"2.0","id":1,"method":"session.create"}' | pnpm nanoagent-rpc
echo '{"jsonrpc":"2.0","id":1,"method":"session.resume","params":{"sessionId":"<session-id>"}}' | pnpm nanoagent-rpc
echo '{"jsonrpc":"2.0","id":1,"method":"message.send","params":{"prompt":"Say hello"}}' | pnpm nanoagent-rpc
echo '{"jsonrpc":"2.0","id":1,"method":"permission.respond","params":{"token":"...","decision":"allow"}}' | pnpm nanoagent-rpc
pnpm nanoagent:tui
echo '{"jsonrpc":"2.0","id":1,"method":"runtime.info"}' | pnpm nanoagent:rpcsrc/agent/: shared runtime boundary, tool loop, approval flow, and session continuationsrc/cli/: one-shot chat CLI entrypointsrc/commands/: shared slash command registry for CLI and TUIsrc/mcp/: stdio MCP client, server lifecycle, and remote capability mappingsrc/tui/: multi-turn native terminal UI with scrollback-first renderingsrc/rpc/: JSON-RPC stdin/stdout transportsrc/config/:.envloading and config parsingsrc/config/:.env, layered settings, andCLAUDE.mdmemory loadingsrc/llm/: model client abstractions and Anthropic implementationsrc/tools/: tool registry and execution forLS,Read,Write, andBashsrc/permissions/: permission policy loading and decisionssrc/session/: on-disk session metadata, state, and transcript storagesrc/hooks/: hook bus, config loading, and built-in hooks
Milestone 7 is intentionally focused:
- all interfaces share the same bounded tool-capable runtime
- the default model is Claude Sonnet 4.6 via
claude-sonnet-4-6 - the available tools are
LS,Read,Write, andBash - read-only tools are allowed by default
WriteandBashrequire approval by default- sessions are persisted under
.nanoagent/sessions/<id>/ - approval tokens now point at stored session state instead of serializing the full request state
- lifecycle hooks can observe model calls, tool execution, permission decisions, and session boundaries
- built-in sample hooks include console logging and file-backed event metrics
- runtime settings can be layered from user, project, and local config files
CLAUDE.md-style memory is loaded and injected into the runtime system prompt- CLI and TUI share a slash command layer for built-ins and custom prompt shortcuts
- config-driven stdio MCP servers can contribute tools, resources, and prompts
- the TUI now shows live thinking status, tool calls, tool output, and inline approval prompts in a native terminal flow
Tool activity and permission decisions are returned from the runtime and surfaced by each interface. The RPC surface now includes tools.list, session.resume, and permission.respond in addition to runtime.info, session.create, and message.send.
For MCP inspection, RPC also includes mcp.servers, mcp.resources.list, mcp.resources.read, mcp.prompts.list, and mcp.prompts.get.
Each persisted session directory contains:
metadata.jsonstate.jsontranscript.jsonl
When nanoagent implements a tool that has a clear Claude Code equivalent, it should keep the Claude Code-style tool name and stay as close as practical to the same semantics. The current alignment target includes LS, Read, Write, and Bash.
Hooks are loaded from nanoagent.hooks.json if present.
The built-in hook types are:
console_logger: writes lifecycle events to stderrfile_metrics: writes per-event counters to a JSON fileshell_command: runs an arbitrary bash command when matching events fire
Example:
{
"hooks": [
{
"type": "console_logger"
},
{
"type": "file_metrics",
"outputPath": ".nanoagent/hooks/metrics.json"
},
{
"type": "shell_command",
"command": "printf '%s\n' \"$NANOAGENT_HOOK_EVENT_NAME\" >> .nanoagent/hooks/events.log",
"events": ["session.end", "permission.decision"]
}
]
}For shell_command hooks:
commandis executed with/bin/bash -lceventsis optional; if omitted, the command runs for every lifecycle eventcwdis optional and defaults to the project roottimeoutMsis optional and defaults to10000- the hook process receives
NANOAGENT_HOOK_EVENT_NAME - the hook process receives
NANOAGENT_HOOK_EVENT_JSON
Example event-specific shell hook:
{
"type": "shell_command",
"command": "printf '%s\n' \"$NANOAGENT_HOOK_EVENT_JSON\" >> .nanoagent/hooks/events.jsonl",
"events": ["tool.execution.after"]
}The current lifecycle event names are:
session.startsession.endmodel.request.beforemodel.response.aftertool.execution.beforetool.execution.afterpermission.decision
The package scripts run TypeScript directly through Node's --experimental-strip-types support so the basic interfaces can work without an additional runtime layer.
nanoagent now resolves settings and memory in this order:
Settings files:
- user:
~/.config/nanoagent/settings.json - project:
./nanoagent.settings.json - local:
./nanoagent.settings.local.json - environment variables override file settings
Memory files:
- user:
~/.config/nanoagent/CLAUDE.md - project:
./CLAUDE.md - local:
./CLAUDE.local.md
Memory files are concatenated in that order and injected into the system prompt for new sessions.
You can inspect the effective config and loaded memory sources with:
nanoagent config- RPC
config.inspect
CLI and TUI both support slash commands.
Built-in commands:
/help/config/tools/session
Custom slash commands can be defined in layered settings files:
{
"slashCommands": {
"review": {
"description": "Review the current workspace changes for bugs and regressions.",
"prompt": "Review the current workspace changes. Focus on bugs, regressions, and missing tests.\n\nAdditional focus:\n{{args}}"
}
}
}{{args}} is replaced with the text after the slash command name. If a prompt does not include {{args}}, trailing arguments are appended automatically.
Examples:
nanoagent chat "/help"nanoagent chat "/review auth and permissions"- in the TUI:
/tools
The TUI is a scrollback-first terminal interface rather than a full-screen dashboard.
Key controls:
Enter: send the current promptEsc: clear the current promptCtrl+C: quit/help: show slash commands/exitor/quit: leave the TUI
During a turn, the footer shows live status changes such as:
- idle
- thinking
- calling a tool
- waiting for approval
- error
Tool calls, tool output, approval requests, and assistant replies are appended to terminal scrollback as compact blocks.
nanoagent supports config-driven MCP servers over stdio.
Example:
{
"mcpServers": {
"docs": {
"transport": "stdio",
"command": "node",
"args": ["./scripts/docs-mcp-server.js"],
"permissionKind": "read"
}
}
}Notes:
- only stdio MCP servers are supported in this milestone
- MCP tools are mapped into the normal tool loop and obey the same permission flow
- server failures are isolated; a broken MCP server should not crash the local runtime
- remote MCP tools are surfaced to the model as
mcp__<server>__<tool>
Useful RPC methods:
mcp.serversmcp.resources.listmcp.resources.readmcp.prompts.listmcp.prompts.get