Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Fleming
66fb080297 chore: break up command handlers 2026-03-13 14:47:53 -06:00
6 changed files with 97 additions and 114 deletions

View File

@@ -42,7 +42,7 @@ mkdir -p ~/.nanobot
**2. Chat** **2. Chat**
```bash ```bash
bun run nanobot agent bun run start agent
``` ```
That's it. That's it.
@@ -54,21 +54,22 @@ That's it.
Chat with the agent from your terminal. Does not require a running gateway. Chat with the agent from your terminal. Does not require a running gateway.
``` ```
bun run nanobot agent [options] bun run start agent [options]
``` ```
| Option | Description | | Option | Description |
|--------|-------------| |--------|-------------|
| `-c, --config <path>` | Path to `config.json` (default: `~/.nanobot/config.json`) | | `-c, --config <path>` | Path to `config.json` (default: `~/.nanobot/config.json`) |
| `-m, --message <text>` | Send a single message and exit (non-interactive) | | `-m, --message <text>` | Send a single message and exit (non-interactive) |
| `-w, --workspace <path>` | Override the workspace directory |
| `-M, --model <model>` | Override the model for this session | | `-M, --model <model>` | Override the model for this session |
**Interactive mode** (default when no `-m` is given): **Interactive mode** (default when no `-m` is given):
```bash ```bash
bun run nanobot agent bun run start agent
bun run nanobot agent -c ~/.nanobot-work/config.json bun run start agent -c ~/.nanobot-work/config.json
bun run nanobot agent -w /tmp/scratch bun run start agent -w /tmp/scratch
``` ```
Press `Ctrl+C` to exit. Press `Ctrl+C` to exit.
@@ -76,8 +77,8 @@ Press `Ctrl+C` to exit.
**Single-shot mode:** **Single-shot mode:**
```bash ```bash
bun run nanobot agent -m "What time is it in Tokyo?" bun run start agent -m "What time is it in Tokyo?"
bun run nanobot agent -m "Summarize the file ./notes.md" bun run start agent -m "Summarize the file ./notes.md"
``` ```
### `gateway` — Mattermost bot ### `gateway` — Mattermost bot
@@ -85,7 +86,7 @@ bun run nanobot agent -m "Summarize the file ./notes.md"
Runs the full stack: Mattermost WebSocket channel, agent loop, cron scheduler, and heartbeat. Runs the full stack: Mattermost WebSocket channel, agent loop, cron scheduler, and heartbeat.
``` ```
bun run nanobot gateway [options] bun run start gateway [options]
``` ```
| Option | Description | | Option | Description |
@@ -93,8 +94,8 @@ bun run nanobot gateway [options]
| `-c, --config <path>` | Path to `config.json` (default: `~/.nanobot/config.json`) | | `-c, --config <path>` | Path to `config.json` (default: `~/.nanobot/config.json`) |
```bash ```bash
bun run nanobot gateway bun run start gateway
bun run nanobot gateway -c ~/.nanobot-work/config.json bun run start gateway -c ~/.nanobot-work/config.json
``` ```
Handles `SIGINT` / `SIGTERM` for graceful shutdown. Handles `SIGINT` / `SIGTERM` for graceful shutdown.
@@ -109,6 +110,7 @@ Environment variable overrides:
|----------|-------------------| |----------|-------------------|
| `NANOBOT_CONFIG` | path to config file | | `NANOBOT_CONFIG` | path to config file |
| `NANOBOT_MODEL` | `agent.model` | | `NANOBOT_MODEL` | `agent.model` |
| `NANOBOT_WORKSPACE` | `agent.workspacePath` |
### Full config reference ### Full config reference
@@ -196,7 +198,7 @@ For Ollama, set `providers.ollama.apiBase` (default: `http://localhost:11434/api
} }
``` ```
4. Run `bun run nanobot gateway` 4. Run `bun run start gateway`
`allowFrom` controls which users the bot responds to. Use `["*"]` to allow all users. `allowFrom` controls which users the bot responds to. Use `["*"]` to allow all users.
@@ -231,10 +233,10 @@ Run separate instances with different configs — useful for isolated workspaces
```bash ```bash
# Instance A # Instance A
bun run nanobot gateway -c ~/.nanobot-a/config.json bun run start gateway -c ~/.nanobot-a/config.json
# Instance B # Instance B
bun run nanobot gateway -c ~/.nanobot-b/config.json 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: Each instance needs its own config file. Set a different `agent.workspacePath` per instance to keep memory, sessions, and cron jobs isolated:
@@ -250,10 +252,10 @@ Each instance needs its own config file. Set a different `agent.workspacePath` p
To run a local CLI session against a specific instance: To run a local CLI session against a specific instance:
```bash ```bash
bun run nanobot agent -c ~/.nanobot-a/config.json -m "Hello" bun run start agent -c ~/.nanobot-a/config.json -m "Hello"
# Temporarily override the workspace for a one-off run # Temporarily override the workspace for a one-off run
bun run nanobot agent -c ~/.nanobot-a/config.json -w /tmp/scratch bun run start agent -c ~/.nanobot-a/config.json -w /tmp/scratch
``` ```
## Linux service (systemd) ## Linux service (systemd)

View File

@@ -49,28 +49,6 @@ Inbound and outbound messages are passed through a typed `AsyncQueue<T>`. The qu
## Logging Pattern ## Logging Pattern
Use `console.error` / `console.warn` / `console.info` / `console.debug` — no external logger. Color via `picocolors` in CLI output only. Use `console.error` / `console.warn` / `console.info` / `console.debug` — no external logger. Color via `picocolors` in CLI output only.
## CLI Command Pattern
Each command is in its own file with a registration function:
```ts
// src/cli/agent.ts
export function agentCommand(program: Command, config: Config, workspace: string): void {
program.command('agent')
.description('...')
.option('-m, --message <text>', 'Single message to process')
.action(async (opts) => { /* ... */ })
}
// src/cli/commands.ts (bootstrap)
export function createCli(): Command {
const program = new Command('nanobot')...
const config = loadConfig(opts.config);
const workspace = resolveWorkspacePath(config.agent.workspacePath);
gatewayCommand(program, config, workspace);
agentCommand(program, config, workspace);
return program;
}
```
## File Layout ## File Layout
``` ```
src/ src/
@@ -89,12 +67,8 @@ src/
tools/base.ts + filesystem.ts + shell.ts + web.ts + message.ts + spawn.ts + cron.ts tools/base.ts + filesystem.ts + shell.ts + web.ts + message.ts + spawn.ts + cron.ts
channels/ channels/
base.ts + mattermost.ts + manager.ts base.ts + mattermost.ts + manager.ts
cli/ cli/commands.ts
types.ts # CommandHandler type index.ts
commands.ts # Bootstrap - loads config, registers commands
agent.ts # agentCommand() - interactive/single-shot mode
gateway.ts # gatewayCommand() - full runtime with Mattermost
index.ts
templates/ (SOUL.md, AGENTS.md, USER.md, TOOLS.md, HEARTBEAT.md, memory/MEMORY.md) templates/ (SOUL.md, AGENTS.md, USER.md, TOOLS.md, HEARTBEAT.md, memory/MEMORY.md)
skills/ (copied from Python repo) skills/ (copied from Python repo)
``` ```

View File

@@ -4,7 +4,7 @@
"type": "module", "type": "module",
"module": "index.ts", "module": "index.ts",
"scripts": { "scripts": {
"nanobot": "bun run index.ts", "start": "bun run index.ts",
"dev": "bun --watch run index.ts", "dev": "bun --watch run index.ts",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"fmt": "oxfmt --check", "fmt": "oxfmt --check",

View File

@@ -1,3 +1,4 @@
import { mkdirSync } from 'node:fs';
import { createInterface } from 'node:readline'; import { createInterface } from 'node:readline';
import { Command } from 'commander'; import { Command } from 'commander';
import pc from 'picocolors'; import pc from 'picocolors';
@@ -7,12 +8,17 @@ import type { Config } from '../config/types.ts';
import { makeProvider } from '../provider/index.ts'; import { makeProvider } from '../provider/index.ts';
export function agentCommand(program: Command, config: Config, workspace: string): void { export function agentCommand(program: Command, config: Config, workspace: string): void {
mkdirSync(workspace, { recursive: true });
program program
.command('agent') .command('agent')
.description('Run the agent interactively or send a single message.') .description('Run the agent interactively or send a single message.')
.option('-c, --config <path>', 'Path to config.json')
.option('-m, --message <text>', 'Single message to process (non-interactive)') .option('-m, --message <text>', 'Single message to process (non-interactive)')
.option('-w, --workspace <path>', 'Workspace path override')
.option('-M, --model <model>', 'Model override') .option('-M, --model <model>', 'Model override')
.action(async (opts: { config?: string; message?: string; model?: string }) => { .action(
async (opts: { config?: string; message?: string; workspace?: string; model?: string }) => {
const model = opts.model ?? config.agent.model; const model = opts.model ?? config.agent.model;
const provider = makeProvider( const provider = makeProvider(
config.providers, config.providers,
@@ -81,5 +87,6 @@ export function agentCommand(program: Command, config: Config, workspace: string
}); });
promptUser(); promptUser();
}); },
);
} }

View File

@@ -3,19 +3,15 @@ import { Command } from 'commander';
import { loadConfig, resolveWorkspacePath } from '../config/loader.ts'; import { loadConfig, resolveWorkspacePath } from '../config/loader.ts';
import { agentCommand } from './agent.ts'; import { agentCommand } from './agent.ts';
import { gatewayCommand } from './gateway.ts'; import { gatewayCommand } from './gateway.ts';
import pc from 'picocolors';
export function createCli(): Command { export function createCli(): Command {
const program = new Command('nanobot') const program = new Command('nanobot')
.description('nanobot — personal AI assistant') .description('nanobot — personal AI assistant')
.option('-c, --config <path>', 'Path to config.json')
.version('1.0.0'); .version('1.0.0');
const globalOpts = program.opts(); const globalOpts = program.opts();
const config = loadConfig(globalOpts.config); const config = loadConfig(globalOpts.config);
const workspace = resolveWorkspacePath(config.agent.workspacePath); const workspace = resolveWorkspacePath(config.agent.workspacePath);
console.info(pc.magenta(`workspace path: ${workspace}`));
mkdirSync(workspace, { recursive: true }); mkdirSync(workspace, { recursive: true });
gatewayCommand(program, config, workspace); gatewayCommand(program, config, workspace);

View File

@@ -1,3 +1,4 @@
import { mkdirSync } from 'node:fs';
import { Command } from 'commander'; import { Command } from 'commander';
import pc from 'picocolors'; import pc from 'picocolors';
import { AgentLoop } from '../agent/loop.ts'; import { AgentLoop } from '../agent/loop.ts';
@@ -10,9 +11,12 @@ import { HeartbeatService } from '../heartbeat/service.ts';
import { makeProvider } from '../provider/index.ts'; import { makeProvider } from '../provider/index.ts';
export function gatewayCommand(program: Command, config: Config, workspace: string): void { export function gatewayCommand(program: Command, config: Config, workspace: string): void {
mkdirSync(workspace, { recursive: true });
program program
.command('gateway') .command('gateway')
.description('Start the full gateway: Mattermost channel, agent loop, cron, and heartbeat.') .description('Start the full gateway: Mattermost channel, agent loop, cron, and heartbeat.')
.option('-c, --config <path>', 'Path to config.json')
.action(async (_opts: { config?: string }) => { .action(async (_opts: { config?: string }) => {
const provider = makeProvider( const provider = makeProvider(
config.providers, config.providers,