diff --git a/README.md b/README.md index 160022a..20dafa5 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,17 @@ bun install # or use `mise install` ## Quick start -**1. Create a config file** +**1. Initialize workspace** ```bash -mkdir -p ~/.config/nanobot +bun run nanobot onboard ``` -`~/.config/nanobot/config.json`: +This creates `~/.config/nanobot/` with a config file and templates. + +**2. Edit config** + +Add your API key and set provider/model: ```json { @@ -34,12 +38,13 @@ mkdir -p ~/.config/nanobot } }, "agent": { - "model": "openrouter/anthropic/claude-sonnet-4-5" + "provider": "openrouter", + "model": "anthropic/claude-sonnet-4-5" } } ``` -**2. Chat** +**3. Chat** ```bash bun run nanobot agent @@ -115,7 +120,8 @@ Environment variable overrides: ```json { "agent": { - "model": "openrouter/anthropic/claude-sonnet-4-5", + "provider": "openrouter", + "model": "anthropic/claude-sonnet-4-5", "workspacePath": "~/.config/nanobot", "maxTokens": 4096, "contextWindowTokens": 65536, @@ -164,17 +170,27 @@ Environment variable overrides: } ``` -### Providers +### Provider -Model names use a `provider/model` prefix scheme: +The `agent.provider` field is **required** and must be one of: -| 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` | +| Provider | Description | +|----------|-------------| +| `anthropic` | Anthropic direct (Claude models) | +| `openai` | OpenAI direct (GPT models) | +| `google` | Google direct (Gemini models) | +| `openrouter` | OpenRouter (access to many models) | +| `ollama` | Local Ollama instance | + +The `agent.model` field is also **required** and should be the model ID without any provider prefix: + +| Provider | Example Model | +|----------|---------------| +| `anthropic` | `claude-sonnet-4-5`, `claude-opus-4-5` | +| `openai` | `gpt-4o`, `gpt-4o-mini` | +| `google` | `gemini-2.5-pro`, `gemini-2.0-flash` | +| `openrouter` | `anthropic/claude-sonnet-4-5` (OpenRouter uses its own model IDs) | +| `ollama` | `llama3.2`, `qwen2.5` | For Ollama, set `providers.ollama.apiBase` (default: `http://localhost:11434/api`). diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 79517c8..239d4c2 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -19,13 +19,14 @@ Docs directory created with 4 files (PRD.md, Architecture.md, API.md, Discoverie - **lint**: all `${err}` in template literals → `${String(err)}`; `String(args['key'] ?? '')` → `strArg(args, 'key')` helper; unused `onProgress` param → `_onProgress`; WebSocket `onerror` `err` type is `Event` → use `err.type` ## Work Queue (next steps) -1. [x] Create workspace helper module (src/workspace.ts) with ensureWorkspace() and syncTemplates() +1. [x] Create workspace helper module (src/cli/utils.ts) with ensureWorkspace() and syncTemplates() 2. [x] Create onboard command (src/cli/onboard.ts) with path argument and directory-not-empty guard -3. [x] Update src/cli/commands.ts to use ensureWorkspace() instead of inline mkdirSync -4. [x] Typecheck and lint pass (0 errors) -5. [x] Runtime smoke test: `bun run nanobot --help` -6. [x] Test onboard command: `bun run nanobot onboard [path]` -7. [ ] Test with a real Mattermost config (optional — user can do this) +3. [x] Agent/gateway commands check workspace exists (throw if not found) +4. [x] Added required `provider` field to agent config (values: anthropic, openai, google, openrouter, ollama) +5. [x] Provider resolution uses explicit provider from config (no model prefix parsing) +6. [x] Typecheck and lint pass (0 errors) +7. [x] Test onboard and agent commands work correctly +8. [ ] Test with a real Mattermost config (optional — user can do this) ## Key Decisions Made - Mattermost channel uses raw WebSocket + fetch (no mattermostdriver, no SSL hack) @@ -34,3 +35,5 @@ Docs directory created with 4 files (PRD.md, Architecture.md, API.md, Discoverie - Config is fresh Zod schema (no migration from Python config needed) - `ollama-ai-provider` package (not `@ai-sdk/ollama` which 404s on npm) - `strArg(args, key, fallback?)` helper exported from `agent/tools/base.ts` for safe unknown→string extraction +- Agent config requires explicit `provider` field (no more model prefix like "anthropic/claude-...") +- Model names are now just the raw model ID (e.g., "claude-sonnet-4-5" not "anthropic/claude-sonnet-4-5") diff --git a/memory-bank/progress.md b/memory-bank/progress.md index a4a5014..10e4af6 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -8,49 +8,20 @@ - All dependencies installed - `src/` directory structure scaffolded - Memory bank initialized -- All source files written (first pass): - - `src/config/types.ts` + `src/config/loader.ts` - - `src/bus/types.ts` + `src/bus/queue.ts` - - `src/provider/types.ts` + `src/provider/index.ts` - - `src/session/types.ts` + `src/session/manager.ts` - - `src/agent/tools/base.ts` (+ `strArg` helper) - - `src/agent/tools/filesystem.ts` - - `src/agent/tools/shell.ts` - - `src/agent/tools/web.ts` - - `src/agent/tools/message.ts` - - `src/agent/tools/spawn.ts` + `src/agent/subagent.ts` - - `src/agent/tools/cron.ts` - - `src/cron/types.ts` + `src/cron/service.ts` - - `src/heartbeat/service.ts` - - `src/agent/memory.ts` - - `src/agent/skills.ts` - - `src/agent/context.ts` - - `src/agent/loop.ts` - - `src/channels/base.ts` + `src/channels/mattermost.ts` - - `src/channels/manager.ts` - - `src/cli/commands.ts` - - `index.ts` +- All source files written (first pass) - Templates and skills copied from Python repo - **Full typecheck pass**: `tsc --noEmit` → 0 errors - **Full lint pass**: `oxlint` → 0 errors, 0 warnings - `package.json` scripts added: `start`, `dev`, `typecheck` - **Docs created**: `/docs/PRD.md`, `Architecture.md`, `API.md`, `Discoveries.md` +- **Onboard command**: Created `src/cli/onboard.ts` with workspace initialization +- **Provider config**: Added required `provider` field to agent config +- **Workspace validation**: Agent/gateway commands throw if workspace doesn't exist ### 🔄 In Progress - Nothing -### ✅ Done -- Created src/workspace.ts with ensureWorkspace(), syncTemplates(), checkWorkspaceEmpty() -- Created src/cli/onboard.ts command with path argument -- Updated src/cli/commands.ts to use ensureWorkspace() helper -- Typecheck: 0 errors -- Lint: 0 warnings - -### 🔄 In Progress -- Testing onboard command - ### ⏳ Pending -- Runtime smoke test: `bun run nanobot --help` - Integration test with a real Mattermost server ## Known Issues / Risks diff --git a/src/cli/agent.ts b/src/cli/agent.ts index 9cba001..7a97c5f 100644 --- a/src/cli/agent.ts +++ b/src/cli/agent.ts @@ -24,6 +24,7 @@ export function agentCommand(program: Command): void { const model = opts.model ?? config.agent.model; const provider = makeProvider( config.providers, + config.agent.provider, model, config.agent.maxTokens, config.agent.temperature, diff --git a/src/cli/gateway.ts b/src/cli/gateway.ts index f528d5e..ac5ed4b 100644 --- a/src/cli/gateway.ts +++ b/src/cli/gateway.ts @@ -24,6 +24,7 @@ export function gatewayCommand(program: Command): void { const provider = makeProvider( config.providers, + config.agent.provider, config.agent.model, config.agent.maxTokens, config.agent.temperature, diff --git a/src/cli/onboard.ts b/src/cli/onboard.ts index ef15c9a..1eadf27 100644 --- a/src/cli/onboard.ts +++ b/src/cli/onboard.ts @@ -2,7 +2,6 @@ import { writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { Command } from 'commander'; import pc from 'picocolors'; -import { ConfigSchema, type Config } from '../config/types.ts'; import { WORKSPACE_PATH } from '../config/constants.ts' import { ensureWorkspace, resolvePath, checkWorkspaceEmpty, syncTemplates } from './utils.ts'; @@ -16,12 +15,14 @@ export function onboardCommand(program: Command): void { .description('Initialize a new nanobot workspace with config and templates') .action(async (rawPath?: string) => { try { - const defaultConfig: Config = ConfigSchema.parse({ + // Create a minimal config template - users must fill in provider and model + const defaultConfig = { + providers: {}, agent: { provider: '', model: '', - } - }); + }, + }; const targetPath = resolvePath(rawPath ?? WORKSPACE_PATH); const configPath = join(targetPath, 'config.json'); diff --git a/src/config/loader.ts b/src/config/loader.ts index 7bc8a05..d491290 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -3,7 +3,6 @@ import { homedir } from 'node:os'; import { dirname, resolve } from 'node:path'; import pc from 'picocolors' import { type Config, ConfigSchema } from './types.ts'; -import { WORKSPACE_PATH } from './constants.ts' const DEFAULT_CONFIG_PATH = resolve(homedir(), '.config', 'nanobot', 'config.json'); diff --git a/src/config/types.ts b/src/config/types.ts index b440464..60261cc 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -40,8 +40,18 @@ export type ChannelsConfig = z.infer; // Agent // --------------------------------------------------------------------------- +export const AgentProviderSchema = z.enum([ + 'anthropic', + 'openai', + 'google', + 'openrouter', + 'ollama', +]); +export type AgentProvider = z.infer; + export const AgentConfigSchema = z.object({ - model: z.string().default('anthropic/claude-sonnet-4-5'), + provider: AgentProviderSchema, + model: z.string(), workspacePath: z.string().default(WORKSPACE_PATH), maxTokens: z.number().int().default(4096), contextWindowTokens: z.number().int().default(65536), diff --git a/src/provider/index.ts b/src/provider/index.ts index 8b58203..3c509aa 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -5,7 +5,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { type ModelMessage, generateText, stepCountIs } from 'ai'; import { jsonrepair } from 'jsonrepair'; import { createOllama } from 'ollama-ai-provider'; -import type { ProvidersConfig } from '../config/types.ts'; +import type { AgentProvider, ProvidersConfig } from '../config/types.ts'; import type { ChatOptions, LLMResponse, ToolDefinition } from './types.ts'; export type { ToolDefinition }; @@ -66,17 +66,20 @@ import type { LanguageModel } from 'ai'; export class LLMProvider { private _providers: ProvidersConfig; + private _provider: AgentProvider; private _defaultModel: string; private _maxTokens: number; private _temperature: number; constructor( providers: ProvidersConfig, + provider: AgentProvider, defaultModel: string, maxTokens = 4096, temperature = 0.7, ) { this._providers = providers; + this._provider = provider; this._defaultModel = defaultModel; this._maxTokens = maxTokens; this._temperature = temperature; @@ -87,39 +90,30 @@ export class LLMProvider { } private _resolveModel(model: string): LanguageModel { - const slashIdx = model.indexOf('/'); - const prefix = slashIdx >= 0 ? model.slice(0, slashIdx) : model; - const remainder = slashIdx >= 0 ? model.slice(slashIdx + 1) : model; - - switch (prefix) { + switch (this._provider) { case 'anthropic': { const cfg = this._providers.anthropic; - return createAnthropic({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder); + return createAnthropic({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model); } case 'openai': { const cfg = this._providers.openai; - return createOpenAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder); + return createOpenAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model); } case 'google': { const cfg = this._providers.google; - return createGoogleGenerativeAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder); + return createGoogleGenerativeAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model); } case 'openrouter': { const cfg = this._providers.openrouter; - return createOpenRouter({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder); + return createOpenRouter({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model); } case 'ollama': { const cfg = this._providers.ollama; // ollama-ai-provider returns LanguageModelV1; cast to LanguageModel (compatible at runtime) return createOllama({ baseURL: cfg?.apiBase ?? 'http://localhost:11434/api' })( - remainder, + model, ) as unknown as LanguageModel; } - default: { - // No recognized prefix — fall through to openai-compatible - const cfg = this._providers.openai; - return createOpenAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model); - } } } @@ -212,11 +206,12 @@ export class LLMProvider { export function makeProvider( providers: ProvidersConfig, + provider: AgentProvider, model: string, maxTokens: number, temperature: number, ): LLMProvider { - return new LLMProvider(providers, model, maxTokens, temperature); + return new LLMProvider(providers, provider, model, maxTokens, temperature); } /** Build a tool-result message to append after executing a tool call. */