Compare commits

...

3 Commits

Author SHA1 Message Date
Joe Fleming
47c4db53af fix: fail to run agent or gateway without config 2026-03-13 20:11:21 -06:00
Joe Fleming
1dd953d17a fix: bug in utils path
add an optional arg to create config path
2026-03-13 20:06:59 -06:00
Joe Fleming
e915dd2922 chore: better welcome messgae, throw on config parse error
add instructions for gateway mode, change config order, and don't
hard-code the config path, pull it from the default config
2026-03-13 20:05:44 -06:00
7 changed files with 34 additions and 22 deletions

View File

@@ -4,6 +4,7 @@ import pc from 'picocolors';
import { AgentLoop } from '../agent/loop.ts'; import { AgentLoop } from '../agent/loop.ts';
import { MessageBus } from '../bus/queue.ts'; import { MessageBus } from '../bus/queue.ts';
import { makeProvider } from '../provider/index.ts'; import { makeProvider } from '../provider/index.ts';
import { ensureWorkspace } from './utils.ts';
import type { Config } from '../config/types.ts'; import type { Config } from '../config/types.ts';
export function agentCommand(program: Command, config: Config, workspace: string): void { export function agentCommand(program: Command, config: Config, workspace: string): void {
@@ -14,6 +15,7 @@ export function agentCommand(program: Command, config: Config, workspace: string
.option('-m, --message <text>', 'Single message to process (non-interactive)') .option('-m, --message <text>', 'Single message to process (non-interactive)')
.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; model?: string }) => {
ensureWorkspace(workspace);
console.info(pc.magenta(`workspace path: ${workspace}`)); console.info(pc.magenta(`workspace path: ${workspace}`));
const model = opts.model ?? config.agent.model; const model = opts.model ?? config.agent.model;

View File

@@ -3,23 +3,18 @@ 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 { onboardCommand } from './onboard.ts'; import { onboardCommand } from './onboard.ts';
import { ensureWorkspace } from './utils.ts';
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')
.version('1.0.0'); .version('1.0.0');
// Register onboard command first (doesn't need config/workspace)
onboardCommand(program); onboardCommand(program);
// load config and get workspace
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);
ensureWorkspace(workspace);
gatewayCommand(program, config, workspace); gatewayCommand(program, config, workspace);
agentCommand(program, config, workspace); agentCommand(program, config, workspace);

View File

@@ -7,6 +7,7 @@ import { MattermostChannel } from '../channels/mattermost.ts';
import { CronService } from '../cron/service.ts'; import { CronService } from '../cron/service.ts';
import { HeartbeatService } from '../heartbeat/service.ts'; import { HeartbeatService } from '../heartbeat/service.ts';
import { makeProvider } from '../provider/index.ts'; import { makeProvider } from '../provider/index.ts';
import { ensureWorkspace } from './utils.ts';
import type { Config } from '../config/types.ts'; import type { Config } from '../config/types.ts';
@@ -16,6 +17,7 @@ export function gatewayCommand(program: Command, config: Config, workspace: stri
.option('-c, --config <path>', 'Path to config.json') .option('-c, --config <path>', 'Path to config.json')
.description('Start the full gateway: Mattermost channel, agent loop, cron, and heartbeat.') .description('Start the full gateway: Mattermost channel, agent loop, cron, and heartbeat.')
.action(async (_opts: { config?: string }) => { .action(async (_opts: { config?: string }) => {
ensureWorkspace(workspace);
console.info(pc.magenta(`workspace path: ${workspace}`)); console.info(pc.magenta(`workspace path: ${workspace}`));
const provider = makeProvider( const provider = makeProvider(

View File

@@ -5,13 +5,19 @@ import pc from 'picocolors';
import { ConfigSchema, type Config } from '../config/types.ts'; import { ConfigSchema, type Config } from '../config/types.ts';
import { ensureWorkspace, resolvePath, checkWorkspaceEmpty, syncTemplates } from './utils.ts'; import { ensureWorkspace, resolvePath, checkWorkspaceEmpty, syncTemplates } from './utils.ts';
function logCreated(item: string) {
console.info(pc.green(` ✓ Created ${item}`));
}
export function onboardCommand(program: Command): void { export function onboardCommand(program: Command): void {
program program
.command('onboard [path]') .command('onboard [path]')
.description('Initialize a new nanobot workspace with config and templates') .description('Initialize a new nanobot workspace with config and templates')
.action(async (rawPath?: string) => { .action(async (rawPath?: string) => {
try { try {
const targetPath = resolvePath(rawPath ?? '~/.config/nanobot'); const defaultConfig: Config = ConfigSchema.parse({});
const targetPath = resolvePath(rawPath ?? defaultConfig.agent.workspacePath);
const configPath = join(targetPath, 'config.json'); const configPath = join(targetPath, 'config.json');
console.info(pc.blue('Initializing nanobot workspace...')); console.info(pc.blue('Initializing nanobot workspace...'));
@@ -21,18 +27,17 @@ export function onboardCommand(program: Command): void {
checkWorkspaceEmpty(targetPath); checkWorkspaceEmpty(targetPath);
// Create workspace directory // Create workspace directory
ensureWorkspace(targetPath); ensureWorkspace(targetPath, true);
console.info(pc.green('✓ Created workspace directory')); logCreated('workspace directory')
// Write default config // Write default config
const defaultConfig: Config = ConfigSchema.parse({});
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8'); writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
console.info(pc.green('✓ Created config.json')); logCreated('config.json')
// Sync templates // Sync templates
const createdFiles = syncTemplates(targetPath); const createdFiles = syncTemplates(targetPath);
for (const file of createdFiles) { for (const file of createdFiles) {
console.info(pc.dim(` Created ${file}`)); logCreated(file)
} }
console.info(); console.info();
@@ -40,10 +45,13 @@ export function onboardCommand(program: Command): void {
console.info(); console.info();
console.info(pc.bold('Next steps:')); console.info(pc.bold('Next steps:'));
console.info(` 1. Edit ${pc.cyan(configPath)} to add your API keys`); console.info(` 1. Edit ${pc.cyan(configPath)} to add your API keys`);
console.info(` 2. Customize ${pc.cyan(join(targetPath, 'USER.md'))} with your preferences`); console.info(` 2. Customize ${pc.cyan(join(targetPath, 'USER.md'))} and ${pc.cyan(join(targetPath, 'SOUL.md'))} with your preferences`);
console.info(` 3. Start chatting: ${pc.cyan('bun run nanobot agent')}`); console.info(` 3. Start chatting: ${pc.cyan('bun run nanobot agent')}`);
console.info(); console.info();
console.info(pc.dim('For Mattermost integration, configure the channels.mattermost section in config.json')); console.info(` -- For gateway mode:`);
console.info(` 1. Edit ${pc.cyan(configPath)} to add your channel config (Mattermost)`);
console.info(` 2. Connect your agent: ${pc.cyan('bun run nanobot gateway')}`);
console.info();
} catch (err) { } catch (err) {
console.error(pc.red(String(err))); console.error(pc.red(String(err)));
process.exit(1); process.exit(1);

View File

@@ -11,19 +11,24 @@ export function resolvePath(raw: string): string {
return resolve(raw); return resolve(raw);
} }
export function ensureWorkspace(rawPath: string): string { export function ensureWorkspace(rawPath: string, createIfMissing = false): string {
const path = resolvePath(rawPath); const path = resolvePath(rawPath);
if (!existsSync(path)) { if (!existsSync(path)) {
mkdirSync(path, { recursive: true }); if (createIfMissing) {
mkdirSync(path, { recursive: true });
} else {
console.error(pc.red(`Workspace does not exist: ${path}\nRun 'nanobot onboard' to initialize.`))
process.exit(1)
}
} }
return path; return path;
} }
export function syncTemplates(workspacePath: string): string[] { export function syncTemplates(workspacePath: string): string[] {
// Get project root relative to this file // Get project root relative to this file (src/cli/utils.ts)
const currentFile = fileURLToPath(import.meta.url); const currentFile = fileURLToPath(import.meta.url);
const srcDir = dirname(currentFile); const srcDir = dirname(currentFile);
const projectRoot = resolve(srcDir, '..'); const projectRoot = resolve(srcDir, '..', '..');
const templatesDir = resolve(projectRoot, 'templates'); const templatesDir = resolve(projectRoot, 'templates');
if (!existsSync(templatesDir)) { if (!existsSync(templatesDir)) {

View File

@@ -20,9 +20,9 @@ export function loadConfig(configPath?: string): Config {
let json: unknown; let json: unknown;
try { try {
json = JSON.parse(raw); json = JSON.parse(raw);
} catch { } catch(error) {
console.error(`Failed to parse config at ${path}`); console.error(`Failed to parse config at ${path}`);
return ConfigSchema.parse({}); throw error
} }
// Apply NANOBOT_ env var overrides before validation // Apply NANOBOT_ env var overrides before validation

View File

@@ -112,6 +112,7 @@ export type HeartbeatConfig = z.infer<typeof HeartbeatConfigSchema>;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const ConfigSchema = z.object({ export const ConfigSchema = z.object({
providers: ProvidersConfigSchema.default(() => ({})),
agent: AgentConfigSchema.default(() => ({ agent: AgentConfigSchema.default(() => ({
model: 'anthropic/claude-sonnet-4-5', model: 'anthropic/claude-sonnet-4-5',
workspacePath: '~/.config/nanobot', workspacePath: '~/.config/nanobot',
@@ -120,13 +121,12 @@ export const ConfigSchema = z.object({
temperature: 0.7, temperature: 0.7,
maxToolIterations: 40, maxToolIterations: 40,
})), })),
providers: ProvidersConfigSchema.default(() => ({})), heartbeat: HeartbeatConfigSchema.default(() => ({ enabled: false, intervalMinutes: 30 })),
channels: ChannelsConfigSchema.default(() => ({ sendProgress: true, sendToolHints: true })), channels: ChannelsConfigSchema.default(() => ({ sendProgress: true, sendToolHints: true })),
tools: ToolsConfigSchema.default(() => ({ tools: ToolsConfigSchema.default(() => ({
exec: { timeout: 120, denyPatterns: [], restrictToWorkspace: false }, exec: { timeout: 120, denyPatterns: [], restrictToWorkspace: false },
web: {}, web: {},
restrictToWorkspace: false, restrictToWorkspace: false,
})), })),
heartbeat: HeartbeatConfigSchema.default(() => ({ enabled: false, intervalMinutes: 30 })),
}); });
export type Config = z.infer<typeof ConfigSchema>; export type Config = z.infer<typeof ConfigSchema>;