Compare commits
10 Commits
3893d88365
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65a7160ba | ||
|
|
74a5e70322 | ||
|
|
b6df31fcbf | ||
|
|
14aa1c1e7f | ||
|
|
47c4db53af | ||
|
|
1dd953d17a | ||
|
|
e915dd2922 | ||
|
|
9ac92ed536 | ||
|
|
398b98393a | ||
|
|
2d99d17d60 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
# dependencies (bun install)
|
# dependencies (bun install)
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# editors
|
||||||
|
.vscode
|
||||||
|
.openvscode-server
|
||||||
|
|
||||||
# output
|
# output
|
||||||
out
|
out
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"correctness": "warn"
|
"correctness": "warn"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"eslint/no-unused-vars": "error"
|
"eslint/no-unused-vars": "error",
|
||||||
|
"unicorn/no-nested-ternary": "error"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"typeAware": true,
|
"typeAware": true,
|
||||||
|
|||||||
72
README.md
72
README.md
@@ -18,13 +18,17 @@ bun install # or use `mise install`
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
**1. Create a config file**
|
**1. Initialize workspace**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.nanobot
|
bun run nanobot onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
`~/.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
|
```json
|
||||||
{
|
{
|
||||||
@@ -34,12 +38,13 @@ mkdir -p ~/.nanobot
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"model": "openrouter/anthropic/claude-sonnet-4-5"
|
"provider": "openrouter",
|
||||||
|
"model": "anthropic/claude-sonnet-4-5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. Chat**
|
**3. Chat**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run nanobot agent
|
bun run nanobot agent
|
||||||
@@ -59,7 +64,7 @@ bun run nanobot 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: `~/.config/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) |
|
||||||
| `-M, --model <model>` | Override the model for this session |
|
| `-M, --model <model>` | Override the model for this session |
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ bun run nanobot agent [options]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run nanobot agent
|
bun run nanobot agent
|
||||||
bun run nanobot agent -c ~/.nanobot-work/config.json
|
bun run nanobot agent -c ~/.config/nanobot-work/config.json
|
||||||
bun run nanobot agent -w /tmp/scratch
|
bun run nanobot agent -w /tmp/scratch
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -90,18 +95,18 @@ bun run nanobot gateway [options]
|
|||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `-c, --config <path>` | Path to `config.json` (default: `~/.nanobot/config.json`) |
|
| `-c, --config <path>` | Path to `config.json` (default: `~/.config/nanobot/config.json`) |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run nanobot gateway
|
bun run nanobot gateway
|
||||||
bun run nanobot gateway -c ~/.nanobot-work/config.json
|
bun run nanobot gateway -c ~/.config/nanobot-work/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Handles `SIGINT` / `SIGTERM` for graceful shutdown.
|
Handles `SIGINT` / `SIGTERM` for graceful shutdown.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Config file: `~/.nanobot/config.json` (or pass `-c <path>` to any command).
|
Config file: `~/.config/nanobot/config.json` (or pass `-c <path>` to any command).
|
||||||
|
|
||||||
Environment variable overrides:
|
Environment variable overrides:
|
||||||
|
|
||||||
@@ -115,8 +120,9 @@ Environment variable overrides:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"agent": {
|
"agent": {
|
||||||
"model": "openrouter/anthropic/claude-sonnet-4-5",
|
"provider": "openrouter",
|
||||||
"workspacePath": "~/.nanobot",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
|
"workspacePath": "~/.config/nanobot",
|
||||||
"maxTokens": 4096,
|
"maxTokens": 4096,
|
||||||
"contextWindowTokens": 65536,
|
"contextWindowTokens": 65536,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
@@ -127,7 +133,7 @@ Environment variable overrides:
|
|||||||
"openai": { "apiKey": "..." },
|
"openai": { "apiKey": "..." },
|
||||||
"google": { "apiKey": "..." },
|
"google": { "apiKey": "..." },
|
||||||
"openrouter": { "apiKey": "..." },
|
"openrouter": { "apiKey": "..." },
|
||||||
"ollama": { "apiBase": "http://localhost:11434/api" }
|
"ollama": { "apiBase": "http://localhost:11434" }
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"sendProgress": true,
|
"sendProgress": true,
|
||||||
@@ -164,19 +170,29 @@ 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 |
|
| Provider | Description |
|
||||||
|--------|----------|---------|
|
|----------|-------------|
|
||||||
| `anthropic/` | Anthropic direct | `anthropic/claude-opus-4-5` |
|
| `anthropic` | Anthropic direct (Claude models) |
|
||||||
| `openai/` | OpenAI direct | `openai/gpt-4o` |
|
| `openai` | OpenAI direct (GPT models) |
|
||||||
| `google/` | Google direct | `google/gemini-2.5-pro` |
|
| `google` | Google direct (Gemini models) |
|
||||||
| `openrouter/` | OpenRouter (any model) | `openrouter/anthropic/claude-sonnet-4-5` |
|
| `openrouter` | OpenRouter (access to many models) |
|
||||||
| `ollama/` | Local Ollama | `ollama/llama3.2` |
|
| `ollama` | Local Ollama instance |
|
||||||
|
|
||||||
For Ollama, set `providers.ollama.apiBase` (default: `http://localhost:11434/api`).
|
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`).
|
||||||
|
|
||||||
### Mattermost setup
|
### Mattermost setup
|
||||||
|
|
||||||
@@ -231,10 +247,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 nanobot gateway -c ~/.config/nanobot-a/config.json
|
||||||
|
|
||||||
# Instance B
|
# Instance B
|
||||||
bun run nanobot gateway -c ~/.nanobot-b/config.json
|
bun run nanobot gateway -c ~/.config/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:
|
||||||
@@ -242,7 +258,7 @@ Each instance needs its own config file. Set a different `agent.workspacePath` p
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"agent": {
|
"agent": {
|
||||||
"workspacePath": "~/.nanobot-a"
|
"workspacePath": "~/.config/nanobot-a"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -250,10 +266,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 nanobot agent -c ~/.config/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 nanobot agent -c ~/.config/nanobot-a/config.json -w /tmp/scratch
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linux service (systemd)
|
## Linux service (systemd)
|
||||||
|
|||||||
18
bun.lock
18
bun.lock
@@ -11,12 +11,12 @@
|
|||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"@openrouter/ai-sdk-provider": "^2.3.0",
|
"@openrouter/ai-sdk-provider": "^2.3.0",
|
||||||
"ai": "^6.0.116",
|
"ai": "^6.0.116",
|
||||||
|
"ai-sdk-ollama": "^3.8.0",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.5.0",
|
||||||
"js-tiktoken": "^1.0.21",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jsonrepair": "^3.13.3",
|
"jsonrepair": "^3.13.3",
|
||||||
"node-html-parser": "^7.1.0",
|
"node-html-parser": "^7.1.0",
|
||||||
"ollama-ai-provider": "^1.2.0",
|
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
@@ -151,6 +151,8 @@
|
|||||||
|
|
||||||
"ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
|
"ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
|
||||||
|
|
||||||
|
"ai-sdk-ollama": ["ai-sdk-ollama@3.8.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.15", "jsonrepair": "^3.13.2", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.89" } }, "sha512-Nlla8FpK8QFMNh9m8sPCZoNqnr+n+Ud0QTqpXNds4j/b/lbVZGaji13ZcRuuFvBwPwd4xnFkNrijJzi70Ih1Tg=="],
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||||
@@ -187,13 +189,11 @@
|
|||||||
|
|
||||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
|
||||||
|
|
||||||
"node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="],
|
"node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="],
|
||||||
|
|
||||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
||||||
"ollama-ai-provider": ["ollama-ai-provider@1.2.0", "", { "dependencies": { "@ai-sdk/provider": "^1.0.0", "@ai-sdk/provider-utils": "^2.0.0", "partial-json": "0.1.7" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww=="],
|
"ollama": ["ollama@0.6.3", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg=="],
|
||||||
|
|
||||||
"oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="],
|
"oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="],
|
||||||
|
|
||||||
@@ -201,22 +201,16 @@
|
|||||||
|
|
||||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.16.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.16.0", "@oxlint-tsgolint/darwin-x64": "0.16.0", "@oxlint-tsgolint/linux-arm64": "0.16.0", "@oxlint-tsgolint/linux-x64": "0.16.0", "@oxlint-tsgolint/win32-arm64": "0.16.0", "@oxlint-tsgolint/win32-x64": "0.16.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-4RuJK2jP08XwqtUu+5yhCbxEauCm6tv2MFHKEMsjbosK2+vy5us82oI3VLuHwbNyZG7ekZA26U2LLHnGR4frIA=="],
|
"oxlint-tsgolint": ["oxlint-tsgolint@0.16.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.16.0", "@oxlint-tsgolint/darwin-x64": "0.16.0", "@oxlint-tsgolint/linux-arm64": "0.16.0", "@oxlint-tsgolint/linux-x64": "0.16.0", "@oxlint-tsgolint/win32-arm64": "0.16.0", "@oxlint-tsgolint/win32-x64": "0.16.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-4RuJK2jP08XwqtUu+5yhCbxEauCm6tv2MFHKEMsjbosK2+vy5us82oI3VLuHwbNyZG7ekZA26U2LLHnGR4frIA=="],
|
||||||
|
|
||||||
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
|
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
|
||||||
|
|
||||||
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
"ollama-ai-provider/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
|
||||||
|
|
||||||
"ollama-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ Wraps Vercel AI SDK `generateText()` with:
|
|||||||
- Normalized `LLMResponse` type
|
- Normalized `LLMResponse` type
|
||||||
|
|
||||||
### SessionManager
|
### SessionManager
|
||||||
Persists conversation history to JSONL files in `~/.nanobot/sessions/`.
|
Persists conversation history to JSONL files in `~/.config/nanobot/sessions/`.
|
||||||
|
|
||||||
- Key format: `{channel}:{chatId}` (e.g., `mattermost:abc123`)
|
- Key format: `{channel}:{chatId}` (e.g., `mattermost:abc123`)
|
||||||
- Supports history truncation for context window limits
|
- Supports history truncation for context window limits
|
||||||
@@ -136,7 +136,7 @@ When session history exceeds token limits, summarizes old messages and archives
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- File: `~/.nanobot/config.json`
|
- File: `~/.config/nanobot/config.json`
|
||||||
- Validation: Zod schemas in `src/config/types.ts`
|
- Validation: Zod schemas in `src/config/types.ts`
|
||||||
- Env overrides: `NANOBOT_MODEL`, `NANOBOT_WORKSPACE`, `NANOBOT_CONFIG`
|
- Env overrides: `NANOBOT_MODEL`, `NANOBOT_WORKSPACE`, `NANOBOT_CONFIG`
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const timeout = parseInt(strArg(args, 'timeout', '30'), 10);
|
|||||||
## Session Persistence
|
## Session Persistence
|
||||||
|
|
||||||
- Format: JSONL (one JSON object per line)
|
- Format: JSONL (one JSON object per line)
|
||||||
- Location: `~/.nanobot/sessions/{sessionKey}.jsonl`
|
- Location: `~/.config/nanobot/sessions/{sessionKey}.jsonl`
|
||||||
- Tool results truncated at 16,000 characters
|
- Tool results truncated at 16,000 characters
|
||||||
- Memory consolidation triggered when approaching context window limit
|
- Memory consolidation triggered when approaching context window limit
|
||||||
|
|
||||||
@@ -147,5 +147,5 @@ Max 3 attempts with exponential backoff.
|
|||||||
|
|
||||||
1. CLI flags (`-c`, `-m`, `-w`, `-M`)
|
1. CLI flags (`-c`, `-m`, `-w`, `-M`)
|
||||||
2. Environment variables (`NANOBOT_CONFIG`, `NANOBOT_MODEL`, `NANOBOT_WORKSPACE`)
|
2. Environment variables (`NANOBOT_CONFIG`, `NANOBOT_MODEL`, `NANOBOT_WORKSPACE`)
|
||||||
3. Config file (`~/.nanobot/config.json`)
|
3. Config file (`~/.config/nanobot/config.json`)
|
||||||
4. Zod schema defaults
|
4. Zod schema defaults
|
||||||
|
|||||||
@@ -19,14 +19,22 @@ 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`
|
- **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)
|
## Work Queue (next steps)
|
||||||
1. [ ] Runtime smoke test: `bun run start --help`
|
1. [x] Create workspace helper module (src/cli/utils.ts) with ensureWorkspace() and syncTemplates()
|
||||||
2. [ ] Test with a real Mattermost config (optional — user can do this)
|
2. [x] Create onboard command (src/cli/onboard.ts) with path argument and directory-not-empty guard
|
||||||
3. [ ] Write sample `~/.nanobot/config.json` in README or docs
|
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. [x] Updated Ollama provider from `ollama-ai-provider` to `ai-sdk-ollama`
|
||||||
|
9. [ ] Test with a real Mattermost config (optional — user can do this)
|
||||||
|
|
||||||
## Key Decisions Made
|
## Key Decisions Made
|
||||||
- Mattermost channel uses raw WebSocket + fetch (no mattermostdriver, no SSL hack)
|
- Mattermost channel uses raw WebSocket + fetch (no mattermostdriver, no SSL hack)
|
||||||
- No MCP support (use shell tools / CLI instead)
|
- No MCP support (use shell tools / CLI instead)
|
||||||
- No reasoning/thinking token handling (can add later)
|
- No reasoning/thinking token handling (can add later)
|
||||||
- Config is fresh Zod schema (no migration from Python config needed)
|
- Config is fresh Zod schema (no migration from Python config needed)
|
||||||
- `ollama-ai-provider` package (not `@ai-sdk/ollama` which 404s on npm)
|
- `ai-sdk-ollama` package for Ollama provider (replaced old `ollama-ai-provider`)
|
||||||
- `strArg(args, key, fallback?)` helper exported from `agent/tools/base.ts` for safe unknown→string extraction
|
- `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")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ A personal AI assistant that connects to Mattermost (via WebSocket) and runs an
|
|||||||
## Key design principles (from Python codebase)
|
## Key design principles (from Python codebase)
|
||||||
- Ultra-lightweight: minimal dependencies, small codebase
|
- Ultra-lightweight: minimal dependencies, small codebase
|
||||||
- Provider-agnostic: works with Anthropic, OpenAI, Google, Ollama, OpenRouter
|
- Provider-agnostic: works with Anthropic, OpenAI, Google, Ollama, OpenRouter
|
||||||
- Workspace-centric: everything lives in a configurable workspace directory (`~/.nanobot/`)
|
- Workspace-centric: everything lives in a configurable workspace directory (`~/.config/nanobot/`)
|
||||||
- SOUL/AGENTS/USER/TOOLS.md: workspace markdown files that define the bot's personality and rules
|
- 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
|
- Memory is just markdown files (`MEMORY.md`, `HISTORY.md`) — no database
|
||||||
|
|
||||||
|
|||||||
@@ -8,41 +8,21 @@
|
|||||||
- All dependencies installed
|
- All dependencies installed
|
||||||
- `src/` directory structure scaffolded
|
- `src/` directory structure scaffolded
|
||||||
- Memory bank initialized
|
- Memory bank initialized
|
||||||
- All source files written (first pass):
|
- 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
|
- Templates and skills copied from Python repo
|
||||||
- **Full typecheck pass**: `tsc --noEmit` → 0 errors
|
- **Full typecheck pass**: `tsc --noEmit` → 0 errors
|
||||||
- **Full lint pass**: `oxlint` → 0 errors, 0 warnings
|
- **Full lint pass**: `oxlint` → 0 errors, 0 warnings
|
||||||
- `package.json` scripts added: `start`, `dev`, `typecheck`
|
- `package.json` scripts added: `start`, `dev`, `typecheck`
|
||||||
- **Docs created**: `/docs/PRD.md`, `Architecture.md`, `API.md`, `Discoveries.md`
|
- **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
|
### 🔄 In Progress
|
||||||
- Nothing
|
- Nothing
|
||||||
|
|
||||||
### ⏳ Pending
|
### ⏳ Pending
|
||||||
- Runtime smoke test: `bun run start --help`
|
|
||||||
- Integration test with a real Mattermost server
|
- Integration test with a real Mattermost server
|
||||||
- Sample `~/.nanobot/config.json` documentation
|
|
||||||
|
|
||||||
## Known Issues / Risks
|
## 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.
|
- `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.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Inbound and outbound messages are passed through a typed `AsyncQueue<T>`. The qu
|
|||||||
- Returns a normalized `LLMResponse` type
|
- Returns a normalized `LLMResponse` type
|
||||||
|
|
||||||
## Config Pattern
|
## Config Pattern
|
||||||
- Config file: `~/.nanobot/config.json` (camelCase JSON)
|
- Config file: `~/.config/nanobot/config.json` (camelCase JSON)
|
||||||
- Loaded with `loadConfig()`, validated by Zod, returns inferred `Config` type
|
- Loaded with `loadConfig()`, validated by Zod, returns inferred `Config` type
|
||||||
- `NANOBOT_` env vars can override fields (e.g. `NANOBOT_MODEL`)
|
- `NANOBOT_` env vars can override fields (e.g. `NANOBOT_MODEL`)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"fmt": "oxfmt --check",
|
"fmt": "oxfmt --check",
|
||||||
"fmt:fix": "oxfmt",
|
"fmt:fix": "oxfmt",
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"lint:fix": "oxlint --fix"
|
"lint:fix": "oxlint --fix",
|
||||||
|
"checks": "bun run lint && bun run fmt"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.58",
|
"@ai-sdk/anthropic": "^3.0.58",
|
||||||
@@ -19,12 +20,12 @@
|
|||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"@openrouter/ai-sdk-provider": "^2.3.0",
|
"@openrouter/ai-sdk-provider": "^2.3.0",
|
||||||
"ai": "^6.0.116",
|
"ai": "^6.0.116",
|
||||||
|
"ai-sdk-ollama": "^3.8.0",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.5.0",
|
||||||
"js-tiktoken": "^1.0.21",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jsonrepair": "^3.13.3",
|
"jsonrepair": "^3.13.3",
|
||||||
"node-html-parser": "^7.1.0",
|
"node-html-parser": "^7.1.0",
|
||||||
"ollama-ai-provider": "^1.2.0",
|
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,21 +27,21 @@ npx --yes clawhub@latest search "web scraping" --limit 5
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx --yes clawhub@latest install <slug> --workdir ~/.nanobot/workspace
|
npx --yes clawhub@latest install <slug> --workdir ~/.config/nanobot/workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `<slug>` with the skill name from search results. This places the skill into `~/.nanobot/workspace/skills/`, where nanobot loads workspace skills from. Always include `--workdir`.
|
Replace `<slug>` with the skill name from search results. This places the skill into `~/.config/nanobot/workspace/skills/`, where nanobot loads workspace skills from. Always include `--workdir`.
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx --yes clawhub@latest update --all --workdir ~/.nanobot/workspace
|
npx --yes clawhub@latest update --all --workdir ~/.config/nanobot/workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
## List installed
|
## List installed
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx --yes clawhub@latest list --workdir ~/.nanobot/workspace
|
npx --yes clawhub@latest list --workdir ~/.config/nanobot/workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -49,5 +49,5 @@ npx --yes clawhub@latest list --workdir ~/.nanobot/workspace
|
|||||||
- Requires Node.js (`npx` comes with it).
|
- Requires Node.js (`npx` comes with it).
|
||||||
- No API key needed for search and install.
|
- No API key needed for search and install.
|
||||||
- Login (`npx --yes clawhub@latest login`) is only required for publishing.
|
- Login (`npx --yes clawhub@latest login`) is only required for publishing.
|
||||||
- `--workdir ~/.nanobot/workspace` is critical — without it, skills install to the current directory instead of the nanobot workspace.
|
- `--workdir ~/.config/nanobot/workspace` is critical — without it, skills install to the current directory instead of the nanobot workspace.
|
||||||
- After install, remind the user to start a new session to load the skill.
|
- After install, remind the user to start a new session to load the skill.
|
||||||
|
|||||||
@@ -364,13 +364,13 @@ export class AgentLoop {
|
|||||||
if (response.content) await onProgress(response.content);
|
if (response.content) await onProgress(response.content);
|
||||||
const hint = response.toolCalls
|
const hint = response.toolCalls
|
||||||
.map((tc) => {
|
.map((tc) => {
|
||||||
|
let display = '';
|
||||||
|
|
||||||
const firstVal = Object.values(tc.arguments)[0];
|
const firstVal = Object.values(tc.arguments)[0];
|
||||||
const display =
|
if (typeof firstVal === 'string') {
|
||||||
typeof firstVal === 'string'
|
display = `"${firstVal.slice(0, 40) + (firstVal.length > 40 ? '…' : '')}"`;
|
||||||
? firstVal.length > 40
|
}
|
||||||
? `"${firstVal.slice(0, 40)}…"`
|
|
||||||
: `"${firstVal}"`
|
|
||||||
: '';
|
|
||||||
return `${tc.name}(${display})`;
|
return `${tc.name}(${display})`;
|
||||||
})
|
})
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|||||||
@@ -3,19 +3,28 @@ import { Command } from 'commander';
|
|||||||
import pc from 'picocolors';
|
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 type { Config } from '../config/types.ts';
|
|
||||||
import { makeProvider } from '../provider/index.ts';
|
import { makeProvider } from '../provider/index.ts';
|
||||||
|
import { loadConfig } from '../config/loader.ts';
|
||||||
|
import { ensureWorkspace } from './utils.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.')
|
||||||
|
.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('-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);
|
||||||
|
|
||||||
|
console.info(pc.magenta(`workspace path: ${workspace}`));
|
||||||
|
|
||||||
const model = opts.model ?? config.agent.model;
|
const model = opts.model ?? config.agent.model;
|
||||||
const provider = makeProvider(
|
const provider = makeProvider(
|
||||||
config.providers,
|
config.providers,
|
||||||
|
config.agent.provider,
|
||||||
model,
|
model,
|
||||||
config.agent.maxTokens,
|
config.agent.maxTokens,
|
||||||
config.agent.temperature,
|
config.agent.temperature,
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
import { mkdirSync } from 'node:fs';
|
|
||||||
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 pc from 'picocolors';
|
import { onboardCommand } from './onboard.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')
|
||||||
.option('-c, --config <path>', 'Path to config.json')
|
|
||||||
.version('1.0.0');
|
.version('1.0.0');
|
||||||
|
|
||||||
const globalOpts = program.opts();
|
onboardCommand(program);
|
||||||
const config = loadConfig(globalOpts.config);
|
gatewayCommand(program);
|
||||||
const workspace = resolveWorkspacePath(config.agent.workspacePath);
|
agentCommand(program);
|
||||||
|
|
||||||
console.info(pc.magenta(`workspace path: ${workspace}`));
|
|
||||||
mkdirSync(workspace, { recursive: true });
|
|
||||||
|
|
||||||
gatewayCommand(program, config, workspace);
|
|
||||||
agentCommand(program, config, workspace);
|
|
||||||
|
|
||||||
return program;
|
return program;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
|
import { ChannelManager } from '../channels/manager.ts';
|
||||||
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';
|
||||||
import { MessageBus } from '../bus/queue.ts';
|
import { MessageBus } from '../bus/queue.ts';
|
||||||
import { MattermostChannel } from '../channels/mattermost.ts';
|
import { MattermostChannel } from '../channels/mattermost.ts';
|
||||||
import { ChannelManager } from '../channels/manager.ts';
|
|
||||||
import type { Config } from '../config/types.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';
|
||||||
|
|
||||||
export function gatewayCommand(program: Command, config: Config, workspace: string): void {
|
export function gatewayCommand(program: Command): void {
|
||||||
program
|
program
|
||||||
.command('gateway')
|
.command('gateway')
|
||||||
|
.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);
|
||||||
|
|
||||||
|
console.info(pc.magenta(`workspace path: ${workspace}`));
|
||||||
|
|
||||||
const provider = makeProvider(
|
const provider = makeProvider(
|
||||||
config.providers,
|
config.providers,
|
||||||
|
config.agent.provider,
|
||||||
config.agent.model,
|
config.agent.model,
|
||||||
config.agent.maxTokens,
|
config.agent.maxTokens,
|
||||||
config.agent.temperature,
|
config.agent.temperature,
|
||||||
|
|||||||
69
src/cli/onboard.ts
Normal file
69
src/cli/onboard.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { writeFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import pc from 'picocolors';
|
||||||
|
import { WORKSPACE_PATH } from '../config/constants.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 {
|
||||||
|
program
|
||||||
|
.command('onboard [path]')
|
||||||
|
.description('Initialize a new nanobot workspace with config and templates')
|
||||||
|
.action(async (rawPath?: string) => {
|
||||||
|
try {
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
console.info(pc.blue('Initializing nanobot workspace...'));
|
||||||
|
console.info(pc.dim(`Target path: ${targetPath}`));
|
||||||
|
|
||||||
|
// Check if directory exists and is not empty
|
||||||
|
checkWorkspaceEmpty(targetPath);
|
||||||
|
|
||||||
|
// Create workspace directory
|
||||||
|
ensureWorkspace(targetPath, true);
|
||||||
|
logCreated('workspace directory');
|
||||||
|
|
||||||
|
// Write default config
|
||||||
|
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
||||||
|
logCreated('config.json');
|
||||||
|
|
||||||
|
// Sync templates
|
||||||
|
const createdFiles = syncTemplates(targetPath);
|
||||||
|
for (const file of createdFiles) {
|
||||||
|
logCreated(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info();
|
||||||
|
console.info(pc.green('nanobot workspace initialized successfully!'));
|
||||||
|
console.info();
|
||||||
|
console.info(pc.bold('Next steps:'));
|
||||||
|
console.info(` 1. Edit ${pc.cyan(configPath)} to add your API keys`);
|
||||||
|
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();
|
||||||
|
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) {
|
||||||
|
console.error(pc.red(String(err)));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
89
src/cli/utils.ts
Normal file
89
src/cli/utils.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { dirname, join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import pc from 'picocolors';
|
||||||
|
|
||||||
|
export function resolvePath(raw: string): string {
|
||||||
|
if (raw.startsWith('~/') || raw === '~') {
|
||||||
|
return resolve(homedir(), raw.slice(2));
|
||||||
|
}
|
||||||
|
return resolve(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureWorkspace(rawPath: string, createIfMissing = false): string {
|
||||||
|
const path = resolvePath(rawPath);
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncTemplates(workspacePath: string): string[] {
|
||||||
|
// Get project root relative to this file (src/cli/utils.ts)
|
||||||
|
const currentFile = fileURLToPath(import.meta.url);
|
||||||
|
const srcDir = dirname(currentFile);
|
||||||
|
const projectRoot = resolve(srcDir, '..', '..');
|
||||||
|
const templatesDir = resolve(projectRoot, 'templates');
|
||||||
|
|
||||||
|
if (!existsSync(templatesDir)) {
|
||||||
|
throw new Error(`Templates directory not found at ${templatesDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const created: string[] = [];
|
||||||
|
|
||||||
|
function copyTemplate(src: string, dest: string) {
|
||||||
|
if (existsSync(dest)) return;
|
||||||
|
mkdirSync(dirname(dest), { recursive: true });
|
||||||
|
const content = readFileSync(src, 'utf8');
|
||||||
|
writeFileSync(dest, content, 'utf8');
|
||||||
|
created.push(dest.slice(workspacePath.length + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDir(srcDir: string, destDir: string) {
|
||||||
|
if (!existsSync(srcDir)) return;
|
||||||
|
const entries = readdirSync(srcDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const srcPath = join(srcDir, entry.name);
|
||||||
|
const destPath = join(destDir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyDir(srcPath, destPath);
|
||||||
|
} else if (entry.name.endsWith('.md')) {
|
||||||
|
copyTemplate(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDir(templatesDir, workspacePath);
|
||||||
|
|
||||||
|
// Create empty HISTORY.md
|
||||||
|
const historyPath = join(workspacePath, 'memory', 'HISTORY.md');
|
||||||
|
if (!existsSync(historyPath)) {
|
||||||
|
mkdirSync(dirname(historyPath), { recursive: true });
|
||||||
|
writeFileSync(historyPath, '# Conversation History\n\n', 'utf8');
|
||||||
|
created.push('memory/HISTORY.md');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create skills directory
|
||||||
|
const skillsPath = join(workspacePath, 'skills');
|
||||||
|
if (!existsSync(skillsPath)) {
|
||||||
|
mkdirSync(skillsPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkWorkspaceEmpty(path: string): void {
|
||||||
|
if (!existsSync(path)) return;
|
||||||
|
const entries = readdirSync(path);
|
||||||
|
if (entries.length > 0) {
|
||||||
|
throw new Error(pc.red(`Directory not empty: ${path}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/config/constants.ts
Normal file
1
src/config/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const WORKSPACE_PATH = '~/.config/nanobot';
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
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';
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH = resolve(homedir(), '.nanobot', 'config.json');
|
const DEFAULT_CONFIG_PATH = resolve(homedir(), '.config', 'nanobot', 'config.json');
|
||||||
|
|
||||||
export function getConfigPath(override?: string): string {
|
export function getConfigPath(override?: string): string {
|
||||||
return override ?? process.env['NANOBOT_CONFIG'] ?? DEFAULT_CONFIG_PATH;
|
return override ?? process.env['NANOBOT_CONFIG'] ?? DEFAULT_CONFIG_PATH;
|
||||||
@@ -13,16 +14,17 @@ 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');
|
||||||
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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { WORKSPACE_PATH } from './constants.ts';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Mattermost
|
// Mattermost
|
||||||
@@ -39,9 +40,19 @@ export type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;
|
|||||||
// Agent
|
// Agent
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const AgentProviderSchema = z.enum([
|
||||||
|
'anthropic',
|
||||||
|
'openai',
|
||||||
|
'google',
|
||||||
|
'openrouter',
|
||||||
|
'ollama',
|
||||||
|
]);
|
||||||
|
export type AgentProvider = z.infer<typeof AgentProviderSchema>;
|
||||||
|
|
||||||
export const AgentConfigSchema = z.object({
|
export const AgentConfigSchema = z.object({
|
||||||
model: z.string().default('anthropic/claude-sonnet-4-5'),
|
provider: AgentProviderSchema,
|
||||||
workspacePath: z.string().default('~/.nanobot'),
|
model: z.string(),
|
||||||
|
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),
|
||||||
@@ -112,21 +123,14 @@ export type HeartbeatConfig = z.infer<typeof HeartbeatConfigSchema>;
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const ConfigSchema = z.object({
|
export const ConfigSchema = z.object({
|
||||||
agent: AgentConfigSchema.default(() => ({
|
|
||||||
model: 'anthropic/claude-sonnet-4-5',
|
|
||||||
workspacePath: '~/.nanobot',
|
|
||||||
maxTokens: 4096,
|
|
||||||
contextWindowTokens: 65536,
|
|
||||||
temperature: 0.7,
|
|
||||||
maxToolIterations: 40,
|
|
||||||
})),
|
|
||||||
providers: ProvidersConfigSchema.default(() => ({})),
|
providers: ProvidersConfigSchema.default(() => ({})),
|
||||||
|
agent: AgentConfigSchema,
|
||||||
|
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>;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { createOpenAI } from '@ai-sdk/openai';
|
|||||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
||||||
import { type ModelMessage, generateText, stepCountIs } from 'ai';
|
import { type ModelMessage, generateText, stepCountIs } from 'ai';
|
||||||
import { jsonrepair } from 'jsonrepair';
|
import { jsonrepair } from 'jsonrepair';
|
||||||
import { createOllama } from 'ollama-ai-provider';
|
import { createOllama } from 'ai-sdk-ollama';
|
||||||
import type { ProvidersConfig } from '../config/types.ts';
|
import type { AgentProvider, ProvidersConfig } from '../config/types.ts';
|
||||||
import type { ChatOptions, LLMResponse, ToolDefinition } from './types.ts';
|
import type { ChatOptions, LLMResponse, ToolDefinition } from './types.ts';
|
||||||
|
|
||||||
export type { ToolDefinition };
|
export type { ToolDefinition };
|
||||||
@@ -66,17 +66,20 @@ import type { LanguageModel } from 'ai';
|
|||||||
|
|
||||||
export class LLMProvider {
|
export class LLMProvider {
|
||||||
private _providers: ProvidersConfig;
|
private _providers: ProvidersConfig;
|
||||||
|
private _provider: AgentProvider;
|
||||||
private _defaultModel: string;
|
private _defaultModel: string;
|
||||||
private _maxTokens: number;
|
private _maxTokens: number;
|
||||||
private _temperature: number;
|
private _temperature: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
providers: ProvidersConfig,
|
providers: ProvidersConfig,
|
||||||
|
provider: AgentProvider,
|
||||||
defaultModel: string,
|
defaultModel: string,
|
||||||
maxTokens = 4096,
|
maxTokens = 4096,
|
||||||
temperature = 0.7,
|
temperature = 0.7,
|
||||||
) {
|
) {
|
||||||
this._providers = providers;
|
this._providers = providers;
|
||||||
|
this._provider = provider;
|
||||||
this._defaultModel = defaultModel;
|
this._defaultModel = defaultModel;
|
||||||
this._maxTokens = maxTokens;
|
this._maxTokens = maxTokens;
|
||||||
this._temperature = temperature;
|
this._temperature = temperature;
|
||||||
@@ -87,38 +90,26 @@ export class LLMProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _resolveModel(model: string): LanguageModel {
|
private _resolveModel(model: string): LanguageModel {
|
||||||
const slashIdx = model.indexOf('/');
|
switch (this._provider) {
|
||||||
const prefix = slashIdx >= 0 ? model.slice(0, slashIdx) : model;
|
|
||||||
const remainder = slashIdx >= 0 ? model.slice(slashIdx + 1) : model;
|
|
||||||
|
|
||||||
switch (prefix) {
|
|
||||||
case 'anthropic': {
|
case 'anthropic': {
|
||||||
const cfg = this._providers.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': {
|
case 'openai': {
|
||||||
const cfg = this._providers.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': {
|
case 'google': {
|
||||||
const cfg = this._providers.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': {
|
case 'openrouter': {
|
||||||
const cfg = this._providers.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': {
|
case 'ollama': {
|
||||||
const cfg = this._providers.ollama;
|
const cfg = this._providers.ollama;
|
||||||
// ollama-ai-provider returns LanguageModelV1; cast to LanguageModel (compatible at runtime)
|
return createOllama({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model);
|
||||||
return createOllama({ baseURL: cfg?.apiBase ?? 'http://localhost:11434/api' })(
|
|
||||||
remainder,
|
|
||||||
) 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,17 +137,15 @@ export class LLMProvider {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let toolChoice: 'required' | 'none' | 'auto' = 'auto';
|
||||||
|
if (opts.toolChoice === 'required' || opts.toolChoice === 'none')
|
||||||
|
toolChoice = opts.toolChoice;
|
||||||
const result = await generateText({
|
const result = await generateText({
|
||||||
model,
|
model,
|
||||||
messages: opts.messages as ModelMessage[],
|
messages: opts.messages as ModelMessage[],
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: AI SDK tools type is complex
|
// biome-ignore lint/suspicious/noExplicitAny: AI SDK tools type is complex
|
||||||
tools: aiTools as any,
|
tools: aiTools as any,
|
||||||
toolChoice:
|
toolChoice,
|
||||||
opts.toolChoice === 'required'
|
|
||||||
? 'required'
|
|
||||||
: opts.toolChoice === 'none'
|
|
||||||
? 'none'
|
|
||||||
: 'auto',
|
|
||||||
maxOutputTokens: maxTokens,
|
maxOutputTokens: maxTokens,
|
||||||
temperature,
|
temperature,
|
||||||
stopWhen: stepCountIs(1),
|
stopWhen: stepCountIs(1),
|
||||||
@@ -215,11 +204,12 @@ export class LLMProvider {
|
|||||||
|
|
||||||
export function makeProvider(
|
export function makeProvider(
|
||||||
providers: ProvidersConfig,
|
providers: ProvidersConfig,
|
||||||
|
provider: AgentProvider,
|
||||||
model: string,
|
model: string,
|
||||||
maxTokens: number,
|
maxTokens: number,
|
||||||
temperature: number,
|
temperature: number,
|
||||||
): LLMProvider {
|
): 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. */
|
/** Build a tool-result message to append after executing a tool call. */
|
||||||
|
|||||||
Reference in New Issue
Block a user