feat: claude one-shot port from nanobot python codebase (v0.1.4.post4)

This commit is contained in:
Joe Fleming
2026-03-13 08:58:43 -06:00
parent 37c66a1bbf
commit f0252e663e
52 changed files with 5002 additions and 7 deletions

View File

@@ -0,0 +1,32 @@
# Active Context
## Current Focus
All source files written and verified — typecheck and lint are both clean.
## Session State (as of this writing)
- All source files complete and passing `tsc --noEmit` (0 errors) and `oxlint` (0 errors, 0 warnings)
- `package.json` scripts added: `start`, `dev`, `typecheck`
- Ready for runtime / integration testing
## Key Fixes Applied This Session
- **Zod v4 `.default()`**: nested object schemas need factory functions returning full output types (e.g. `.default(() => ({ field: value, ... }))`)
- **Zod v4 `z.record()`**: requires two args: `z.record(z.string(), z.unknown())`
- **AI SDK v6**: `LanguageModelV2``LanguageModel`; `maxTokens``maxOutputTokens`; `maxSteps``stopWhen: stepCountIs(1)`; usage fields: `inputTokens` / `outputTokens` (not `promptTokens` / `completionTokens`)
- **ollama-ai-provider v1.2.0**: returns `LanguageModelV1` — cast with `as unknown as LanguageModel`
- **`js-tiktoken`**: `get_encoding``getEncoding`
- **`web.ts`**: `Document` global not available in Bun/Node — return `Record<string, unknown>` from `makePseudoDocument`, cast at call site
- **`loop.ts`**: syntax error `ContextBuilder(workspace: ...)``new ContextBuilder(opts.workspace)`
- **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. [ ] Runtime smoke test: `bun run start --help`
2. [ ] Test with a real Mattermost config (optional — user can do this)
3. [ ] Write sample `~/.nanobot/config.json` in README or docs
## Key Decisions Made
- Mattermost channel uses raw WebSocket + fetch (no mattermostdriver, no SSL hack)
- No MCP support (use shell tools / CLI instead)
- No reasoning/thinking token handling (can add later)
- 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

View File

@@ -0,0 +1,24 @@
# Product Context
## What nanobot does
A personal AI assistant that connects to Mattermost (via WebSocket) and runs an agent loop. It receives messages, uses an LLM to decide what to do, calls tools, and replies. It can also run on a schedule (cron) and proactively act on a heartbeat timer.
## Core user experience
- User sends a message in Mattermost (DM or channel)
- Bot uses the configured LLM, calls tools as needed, replies in the same thread
- Long conversations get automatically consolidated to memory files on disk
- Custom "skills" (markdown instruction files) extend the bot's capabilities
## Key design principles (from Python codebase)
- Ultra-lightweight: minimal dependencies, small codebase
- Provider-agnostic: works with Anthropic, OpenAI, Google, Ollama, OpenRouter
- Workspace-centric: everything lives in a configurable workspace directory (`~/.nanobot/`)
- SOUL/AGENTS/USER/TOOLS.md: workspace markdown files that define the bot's personality and rules
- Memory is just markdown files (`MEMORY.md`, `HISTORY.md`) — no database
## Runtime mental model
```
Mattermost WS ──► inbound queue ──► AgentLoop ──► LLM + tools ──► outbound queue ──► Mattermost REST
CronService + HeartbeatService (inject messages into inbound queue)
```

49
memory-bank/progress.md Normal file
View File

@@ -0,0 +1,49 @@
# Progress
## Milestones
### ✅ Done
- Repo created at `/home/sebby/repos/nanobot-ts`
- Tooling configured: `oxfmt` (single quotes), `oxlint`, `@types/bun`, strict tsconfig
- 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`
- 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`
### 🔄 In Progress
- Nothing
### ⏳ Pending
- Runtime smoke test: `bun run start --help`
- Integration test with a real Mattermost server
- Sample `~/.nanobot/config.json` documentation
## Known Issues / Risks
- `ollama-ai-provider` v1.2.0 returns `LanguageModelV1` (not V2/V3 as expected by AI SDK v6) — cast used at call site. Works at runtime.
- Zod v4 `.default()` on nested object schemas requires providing full output type as factory function return value (not just `{}`). All instances fixed.
- AI SDK v6 changed `maxSteps``stopWhen: stepCountIs(n)` and `usage.promptTokens/completionTokens``usage.inputTokens/outputTokens`.

View File

@@ -0,0 +1,22 @@
# Project Brief
## What
A full port of the Python `nanobot` personal AI agent framework to Bun/TypeScript.
## Why
The owner doesn't know Python and doesn't want to maintain a Python codebase for a small personal project. The port should be idiomatic TypeScript, easier to read, and have fewer dependencies.
## Scope
- **In**: All core features — agent loop, tools, memory/consolidation, skills, sessions, cron, heartbeat, Mattermost channel (WebSocket + REST), config, CLI.
- **Out**: All non-Mattermost channels (Telegram, Discord, Slack, etc.), MCP client support, extended thinking/reasoning tokens, onboard wizard.
## Source of Truth
Original Python implementation lives at `/home/sebby/repos/nanobot`. All porting decisions should be verified against it.
## Constraints
- Bun runtime only (no Node-specific APIs where Bun has equivalents)
- All runtime types written as Zod schemas in dedicated `types.ts` files; those files also export the inferred TS types
- `picocolors` for terminal color (not chalk)
- `oxfmt` for formatting (single quotes), `oxlint` for linting
- Vercel AI SDK (`ai` package) for LLM abstraction
- Mattermost: raw `WebSocket` + `fetch` (no driver library)

View File

@@ -0,0 +1,74 @@
# System Patterns
## Type Definition Pattern
Every module that has runtime-validated data uses a dedicated `types.ts` file:
```
src/config/types.ts ← Zod schemas + `export type X = z.infer<typeof XSchema>`
src/bus/types.ts
src/session/types.ts
src/cron/types.ts
src/provider/types.ts
```
The implementation files (`loader.ts`, `manager.ts`, etc.) import types from the sibling `types.ts`.
## Tool Pattern
All tools implement the `Tool` interface from `src/agent/tools/base.ts`:
```ts
interface Tool {
name: string
description: string
parameters: Record<string, unknown> // JSON Schema object
execute(args: Record<string, unknown>): Promise<string>
}
```
`ToolRegistry` stores tools by name, exposes `getDefinitions()` (OpenAI function-calling format), and `execute(name, args)`.
## Message Bus Pattern
Inbound and outbound messages are passed through a typed `AsyncQueue<T>`. The queue uses a `Promise`-based dequeue that resolves when an item is available (mirrors Python `asyncio.Queue`).
## Provider Pattern
`LLMProvider` (`src/provider/index.ts`) wraps the Vercel AI SDK `generateText()`. It:
- Accepts a model string and resolves it to the correct AI SDK provider instance
- Implements `chatWithRetry()` with 3 attempts on transient errors (429, 5xx, timeout)
- Repairs malformed tool-call JSON with `jsonrepair`
- Returns a normalized `LLMResponse` type
## Config Pattern
- Config file: `~/.nanobot/config.json` (camelCase JSON)
- Loaded with `loadConfig()`, validated by Zod, returns inferred `Config` type
- `NANOBOT_` env vars can override fields (e.g. `NANOBOT_MODEL`)
## Mattermost Channel Pattern
- Inbound: native `WebSocket` connecting to `wss://{server}/api/v4/websocket`, auth via hello message
- Outbound: `fetch()` to `POST /api/v4/posts`
- Session key: `mattermost:{channelId}` (or `mattermost:{channelId}:{rootId}` when `replyInThread`)
## Session Key Convention
`{channel}:{chatId}` — e.g. `mattermost:abc123`, `cli:direct`
## Logging Pattern
Use `console.error` / `console.warn` / `console.info` / `console.debug` — no external logger. Color via `picocolors` in CLI output only.
## File Layout
```
src/
config/types.ts + loader.ts
bus/types.ts + queue.ts
provider/types.ts + index.ts
session/types.ts + manager.ts
cron/types.ts + service.ts
heartbeat/service.ts
agent/
memory.ts
skills.ts
context.ts
loop.ts
subagent.ts
tools/base.ts + filesystem.ts + shell.ts + web.ts + message.ts + spawn.ts + cron.ts
channels/
base.ts + mattermost.ts + manager.ts
cli/commands.ts
index.ts
templates/ (SOUL.md, AGENTS.md, USER.md, TOOLS.md, HEARTBEAT.md, memory/MEMORY.md)
skills/ (copied from Python repo)
```