Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Fleming
b6df31fcbf fix: require a model, default to empty value during onboarding
and add a WORKSPACE_PATH constant to make tracking that path easier
2026-03-13 20:42:05 -06:00
Joe Fleming
14aa1c1e7f fix: correctly load config path from args
log error and exit if config can not be loaded, don't use defaults
2026-03-13 20:29:02 -06:00
7 changed files with 29 additions and 26 deletions

View File

@@ -4,10 +4,10 @@ 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 { loadConfig } from '../config/loader.ts';
import { ensureWorkspace } from './utils.ts'; import { ensureWorkspace } from './utils.ts';
import type { Config } from '../config/types.ts';
export function agentCommand(program: Command, config: Config, workspace: string): void { export function agentCommand(program: Command): void {
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.')
@@ -15,7 +15,10 @@ 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 }) => {
const config = loadConfig(opts.config);
const workspace = config.agent.workspacePath;
ensureWorkspace(workspace); 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

@@ -1,5 +1,4 @@
import { Command } from 'commander'; import { Command } from 'commander';
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';
@@ -10,13 +9,8 @@ export function createCli(): Command {
.version('1.0.0'); .version('1.0.0');
onboardCommand(program); onboardCommand(program);
gatewayCommand(program);
const globalOpts = program.opts(); agentCommand(program);
const config = loadConfig(globalOpts.config);
const workspace = resolveWorkspacePath(config.agent.workspacePath);
gatewayCommand(program, config, workspace);
agentCommand(program, config, workspace);
return program; return program;
} }

View File

@@ -7,17 +7,19 @@ 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 { loadConfig } from '../config/loader.ts';
import { ensureWorkspace } from './utils.ts'; import { ensureWorkspace } from './utils.ts';
import type { Config } from '../config/types.ts'; export function gatewayCommand(program: Command): void {
export function gatewayCommand(program: Command, config: Config, workspace: string): void {
program program
.command('gateway') .command('gateway')
.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 }) => {
const config = loadConfig(opts.config);
const workspace = config.agent.workspacePath;
ensureWorkspace(workspace); ensureWorkspace(workspace);
console.info(pc.magenta(`workspace path: ${workspace}`)); console.info(pc.magenta(`workspace path: ${workspace}`));
const provider = makeProvider( const provider = makeProvider(

View File

@@ -3,6 +3,7 @@ import { join } from 'node:path';
import { Command } from 'commander'; import { Command } from 'commander';
import pc from 'picocolors'; import pc from 'picocolors';
import { ConfigSchema, type Config } from '../config/types.ts'; import { ConfigSchema, type Config } from '../config/types.ts';
import { WORKSPACE_PATH } from '../config/constants.ts'
import { ensureWorkspace, resolvePath, checkWorkspaceEmpty, syncTemplates } from './utils.ts'; import { ensureWorkspace, resolvePath, checkWorkspaceEmpty, syncTemplates } from './utils.ts';
function logCreated(item: string) { function logCreated(item: string) {
@@ -15,9 +16,14 @@ export function onboardCommand(program: Command): void {
.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 defaultConfig: Config = ConfigSchema.parse({}); const defaultConfig: Config = ConfigSchema.parse({
agent: {
provider: '',
model: '',
}
});
const targetPath = resolvePath(rawPath ?? defaultConfig.agent.workspacePath); const targetPath = resolvePath(rawPath ?? WORKSPACE_PATH);
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...'));

1
src/config/constants.ts Normal file
View File

@@ -0,0 +1 @@
export const WORKSPACE_PATH = '~/.config/nanobot'

View File

@@ -1,7 +1,9 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { dirname, resolve } from 'node:path'; import { dirname, resolve } from 'node:path';
import pc from 'picocolors'
import { type Config, ConfigSchema } from './types.ts'; import { type Config, ConfigSchema } from './types.ts';
import { WORKSPACE_PATH } from './constants.ts'
const DEFAULT_CONFIG_PATH = resolve(homedir(), '.config', 'nanobot', 'config.json'); const DEFAULT_CONFIG_PATH = resolve(homedir(), '.config', 'nanobot', 'config.json');
@@ -13,7 +15,8 @@ export function loadConfig(configPath?: string): Config {
const path = getConfigPath(configPath); const path = getConfigPath(configPath);
if (!existsSync(path)) { if (!existsSync(path)) {
return ConfigSchema.parse({}); console.error(pc.red(`Failed to load config from ${configPath}`));
process.exit(1);
} }
const raw = readFileSync(path, 'utf8'); const raw = readFileSync(path, 'utf8');

View File

@@ -1,4 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { WORKSPACE_PATH } from './constants.ts'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mattermost // Mattermost
@@ -41,7 +42,7 @@ export type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;
export const AgentConfigSchema = z.object({ export const AgentConfigSchema = z.object({
model: z.string().default('anthropic/claude-sonnet-4-5'), model: z.string().default('anthropic/claude-sonnet-4-5'),
workspacePath: z.string().default('~/.config/nanobot'), workspacePath: z.string().default(WORKSPACE_PATH),
maxTokens: z.number().int().default(4096), maxTokens: z.number().int().default(4096),
contextWindowTokens: z.number().int().default(65536), contextWindowTokens: z.number().int().default(65536),
temperature: z.number().default(0.7), temperature: z.number().default(0.7),
@@ -113,14 +114,7 @@ export type HeartbeatConfig = z.infer<typeof HeartbeatConfigSchema>;
export const ConfigSchema = z.object({ export const ConfigSchema = z.object({
providers: ProvidersConfigSchema.default(() => ({})), providers: ProvidersConfigSchema.default(() => ({})),
agent: AgentConfigSchema.default(() => ({ agent: AgentConfigSchema,
model: 'anthropic/claude-sonnet-4-5',
workspacePath: '~/.config/nanobot',
maxTokens: 4096,
contextWindowTokens: 65536,
temperature: 0.7,
maxToolIterations: 40,
})),
heartbeat: HeartbeatConfigSchema.default(() => ({ enabled: false, intervalMinutes: 30 })), 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(() => ({