diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 499613625..9b89b69ae 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,7 +2,7 @@ name: build
on:
push:
- branches: ["main"]
+ branches: [ "main" ]
jobs:
build:
@@ -16,10 +16,5 @@ jobs:
with:
go-version-file: go.mod
- - name: fmt
- run: |
- make fmt
- git diff --exit-code || (echo "::error::Code is not formatted. Run 'make fmt' and commit the changes." && exit 1)
-
- name: Build
run: make build-all
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 55bf77e00..be1c10c52 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -24,48 +24,9 @@ jobs:
with:
version: v2.10.1
- # TODO: Remove once linter is properly configured
- fmt-check:
- name: Formatting
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v6
-
- - name: Setup Go
- uses: actions/setup-go@v6
- with:
- go-version-file: go.mod
-
- - name: Check formatting
- run: |
- make fmt
- git diff --exit-code || (echo "::error::Code is not formatted. Run 'make fmt' and commit the changes." && exit 1)
-
- # TODO: Remove once linter is properly configured
- vet:
- name: Vet
- runs-on: ubuntu-latest
- needs: fmt-check
- steps:
- - name: Checkout
- uses: actions/checkout@v6
-
- - name: Setup Go
- uses: actions/setup-go@v6
- with:
- go-version-file: go.mod
-
- - name: Run go generate
- run: go generate ./...
-
- - name: Run go vet
- run: go vet ./...
-
test:
name: Tests
runs-on: ubuntu-latest
- needs: fmt-check
steps:
- name: Checkout
uses: actions/checkout@v6
diff --git a/.golangci.yaml b/.golangci.yaml
index 80e54ac1c..d45d69e67 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -47,7 +47,6 @@ linters:
- godox
- goprintffuncname
- gosec
- - govet
- ineffassign
- lll
- maintidx
@@ -160,12 +159,11 @@ issues:
formatters:
enable:
+ - gci
+ - gofmt
+ - gofumpt
- goimports
- # TODO: Disabled, because they are failing at the moment, we should fix them and enable (step by step)
- # - gci
- # - gofmt
- # - gofumpt
- # - golines
+ - golines
settings:
gci:
sections:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..88227f493
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,302 @@
+# Contributing to PicoClaw
+
+Thank you for your interest in contributing to PicoClaw! This project is a community-driven effort to build the lightweight and versatile personal AI assistant. We welcome contributions of all kinds: bug fixes, features, documentation, translations, and testing.
+
+PicoClaw itself was substantially developed with AI assistance — we embrace this approach and have built our contribution process around it.
+
+## Table of Contents
+
+- [Code of Conduct](#code-of-conduct)
+- [Ways to Contribute](#ways-to-contribute)
+- [Getting Started](#getting-started)
+- [Development Setup](#development-setup)
+- [Making Changes](#making-changes)
+- [AI-Assisted Contributions](#ai-assisted-contributions)
+- [Pull Request Process](#pull-request-process)
+- [Branch Strategy](#branch-strategy)
+- [Code Review](#code-review)
+- [Communication](#communication)
+
+---
+
+## Code of Conduct
+
+We are committed to maintaining a welcoming and respectful community. Be kind, constructive, and assume good faith. Harassment or discrimination of any kind will not be tolerated.
+
+---
+
+## Ways to Contribute
+
+- **Bug reports** — Open an issue using the bug report template.
+- **Feature requests** — Open an issue using the feature request template; discuss before implementing.
+- **Code** — Fix bugs or implement features. See the workflow below.
+- **Documentation** — Improve READMEs, docs, inline comments, or translations.
+- **Testing** — Run PicoClaw on new hardware, channels, or LLM providers and report your results.
+
+For substantial new features, please open an issue first to discuss the design before writing code. This prevents wasted effort and ensures alignment with the project's direction.
+
+---
+
+## Getting Started
+
+1. **Fork** the repository on GitHub.
+2. **Clone** your fork locally:
+ ```bash
+ git clone https://github.com/Cấu hình Zhipu
@@ -1065,7 +1059,7 @@ Thêm key vào `~/.picoclaw/config.json` nếu dùng Brave:
"tools": {
"web": {
"brave": {
- "enabled": true,
+ "enabled": false,
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
},
diff --git a/README.zh.md b/README.zh.md
index 7b5955689..b13569c2d 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -14,7 +14,8 @@
@@ -101,9 +103,12 @@
### 📱 在手机上轻松运行
+
picoclaw 可以将你10年前的老旧手机废物利用,变身成为你的AI助理!快速指南:
+
1. 先去应用商店下载安装Termux
2. 打开后执行指令
+
```bash
# 注意: 下面的v0.1.1 可以换为你实际看到的最新版本
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
@@ -111,19 +116,17 @@ chmod +x picoclaw-linux-arm64
pkg install proot
termux-chroot ./picoclaw-linux-arm64 onboard
```
-然后跟随下面的“快速开始”章节继续配置picoclaw即可使用!
+
+然后跟随下面的“快速开始”章节继续配置picoclaw即可使用!
-
-
-
### 🐜 创新的低占用部署
PicoClaw 几乎可以部署在任何 Linux 设备上!
-* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(网口) 或 W(WiFi6) 版本,用于极简家庭助手。
-* $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html),或 $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html),用于自动化服务器运维。
-* $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) 或 $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera),用于智能监控。
+- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(网口) 或 W(WiFi6) 版本,用于极简家庭助手。
+- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html),或 $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html),用于自动化服务器运维。
+- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) 或 $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera),用于智能监控。
[https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4](https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4)
@@ -242,9 +245,14 @@ picoclaw onboard
],
"tools": {
"web": {
- "search": {
+ "brave": {
+ "enabled": false,
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
+ },
+ "duckduckgo": {
+ "enabled": true,
+ "max_results": 5
}
},
"cron": {
@@ -252,15 +260,14 @@ picoclaw onboard
}
}
}
-
```
-> **新功能**: `model_list` 配置格式支持零代码添加 provider。详见[模型配置](#-模型配置-model_list)章节。
+> **新功能**: `model_list` 配置格式支持零代码添加 provider。详见[模型配置](#模型配置-model_list)章节。
**3. 获取 API Key**
-* **LLM 提供商**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
-* **网络搜索** (可选): [Brave Search](https://brave.com/search/api) - 提供免费层级 (2000 请求/月)
+- **LLM 提供商**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
+- **网络搜索** (可选): [Brave Search](https://brave.com/search/api) - 提供免费层级 (2000 请求/月)
> **注意**: 完整的配置模板请参考 `config.example.json`。
@@ -277,259 +284,28 @@ picoclaw agent -m "2+2 等于几?"
## 💬 聊天应用集成 (Chat Apps)
-通过 Telegram, Discord, 钉钉或企业微信与您的 PicoClaw 对话。
-
-| 渠道 | 设置难度 |
-| --- | --- |
-| **Telegram** | 简单 (仅需 token) |
-| **Discord** | 简单 (bot token + intents) |
-| **QQ** | 简单 (AppID + AppSecret) |
-| **钉钉 (DingTalk)** | 中等 (应用凭证) |
-| **企业微信 (WeCom)** | 中等 (企业ID + Webhook配置) |
-
-
@@ -989,22 +777,25 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
1. 在 [https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查询)
2. 添加到 `~/.picoclaw/config.json`:
+
```json
{
"tools": {
"web": {
- "search": {
+ "brave": {
+ "enabled": false,
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
+ },
+ "duckduckgo": {
+ "enabled": true,
+ "max_results": 5
}
}
}
}
-
```
-
-
### 遇到内容过滤错误 (Content Filtering Errors)
某些提供商(如智谱)有严格的内容过滤。尝试改写您的问题或使用其他模型。
@@ -1017,10 +808,10 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
## 📝 API Key 对比
-| 服务 | 免费层级 | 适用场景 |
-| --- | --- | --- |
-| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
-| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
-| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
-| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
-| **Cerebras** | 提供免费层级 | 极速推理 (Llama, Qwen 等) |
\ No newline at end of file
+| 服务 | 免费层级 | 适用场景 |
+| ---------------- | -------------- | ----------------------------- |
+| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
+| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
+| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
+| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
+| **Cerebras** | 提供免费层级 | 极速推理 (Llama, Qwen 等) |
diff --git a/assets/wechat.png b/assets/wechat.png
index 8fc41ea7d..a34217c33 100644
Binary files a/assets/wechat.png and b/assets/wechat.png differ
diff --git a/cmd/picoclaw/cmd_agent.go b/cmd/picoclaw/cmd_agent.go
index cee9f68ec..6d6ff935f 100644
--- a/cmd/picoclaw/cmd_agent.go
+++ b/cmd/picoclaw/cmd_agent.go
@@ -13,6 +13,7 @@ import (
"strings"
"github.com/chzyer/readline"
+
"github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger"
@@ -74,10 +75,10 @@ func agentCmd() {
// Print agent startup info (only for interactive mode)
startupInfo := agentLoop.GetStartupInfo()
logger.InfoCF("agent", "Agent initialized",
- map[string]interface{}{
- "tools_count": startupInfo["tools"].(map[string]interface{})["count"],
- "skills_total": startupInfo["skills"].(map[string]interface{})["total"],
- "skills_available": startupInfo["skills"].(map[string]interface{})["available"],
+ map[string]any{
+ "tools_count": startupInfo["tools"].(map[string]any)["count"],
+ "skills_total": startupInfo["skills"].(map[string]any)["total"],
+ "skills_available": startupInfo["skills"].(map[string]any)["available"],
})
if message != "" {
@@ -104,7 +105,6 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
InterruptPrompt: "^C",
EOFPrompt: "exit",
})
-
if err != nil {
fmt.Printf("Error initializing readline: %v\n", err)
fmt.Println("Falling back to simple input mode...")
diff --git a/cmd/picoclaw/cmd_auth.go b/cmd/picoclaw/cmd_auth.go
index 5bed7f116..729c56177 100644
--- a/cmd/picoclaw/cmd_auth.go
+++ b/cmd/picoclaw/cmd_auth.go
@@ -114,7 +114,7 @@ func authLoginOpenAI(useDeviceCode bool) {
os.Exit(1)
}
- if err := auth.SetCredential("openai", cred); err != nil {
+ if err = auth.SetCredential("openai", cred); err != nil {
fmt.Printf("Failed to save credentials: %v\n", err)
os.Exit(1)
}
@@ -188,7 +188,7 @@ func authLoginGoogleAntigravity() {
fmt.Printf("Project: %s\n", projectID)
}
- if err := auth.SetCredential("google-antigravity", cred); err != nil {
+ if err = auth.SetCredential("google-antigravity", cred); err != nil {
fmt.Printf("Failed to save credentials: %v\n", err)
os.Exit(1)
}
@@ -265,7 +265,7 @@ func authLoginPasteToken(provider string) {
os.Exit(1)
}
- if err := auth.SetCredential(provider, cred); err != nil {
+ if err = auth.SetCredential(provider, cred); err != nil {
fmt.Printf("Failed to save credentials: %v\n", err)
os.Exit(1)
}
diff --git a/cmd/picoclaw/cmd_gateway.go b/cmd/picoclaw/cmd_gateway.go
index 1f1bf5491..28ef76ad3 100644
--- a/cmd/picoclaw/cmd_gateway.go
+++ b/cmd/picoclaw/cmd_gateway.go
@@ -10,6 +10,7 @@ import (
"os"
"os/signal"
"path/filepath"
+ "strings"
"time"
"github.com/sipeed/picoclaw/pkg/agent"
@@ -60,8 +61,8 @@ func gatewayCmd() {
// Print agent startup info
fmt.Println("\n📦 Agent Status:")
startupInfo := agentLoop.GetStartupInfo()
- toolsInfo := startupInfo["tools"].(map[string]interface{})
- skillsInfo := startupInfo["skills"].(map[string]interface{})
+ toolsInfo := startupInfo["tools"].(map[string]any)
+ skillsInfo := startupInfo["skills"].(map[string]any)
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
fmt.Printf(" • Skills: %d/%d available\n",
skillsInfo["available"],
@@ -69,7 +70,7 @@ func gatewayCmd() {
// Log to file as well
logger.InfoCF("agent", "Agent initialized",
- map[string]interface{}{
+ map[string]any{
"tools_count": toolsInfo["count"],
"skills_total": skillsInfo["total"],
"skills_available": skillsInfo["available"],
@@ -77,7 +78,14 @@ func gatewayCmd() {
// Setup cron tool and service
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
- cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg)
+ cronService := setupCronTool(
+ agentLoop,
+ msgBus,
+ cfg.WorkspacePath(),
+ cfg.Agents.Defaults.RestrictToWorkspace,
+ execTimeout,
+ cfg,
+ )
heartbeatService := heartbeat.NewHeartbeatService(
cfg.WorkspacePath(),
@@ -91,7 +99,8 @@ func gatewayCmd() {
channel, chatID = "cli", "direct"
}
// Use ProcessHeartbeat - no session history, each heartbeat is independent
- response, err := agentLoop.ProcessHeartbeat(context.Background(), prompt, channel, chatID)
+ var response string
+ response, err = agentLoop.ProcessHeartbeat(context.Background(), prompt, channel, chatID)
if err != nil {
return tools.ErrorResult(fmt.Sprintf("Heartbeat error: %v", err))
}
@@ -113,8 +122,17 @@ func gatewayCmd() {
agentLoop.SetChannelManager(channelManager)
var transcriber *voice.GroqTranscriber
- if cfg.Providers.Groq.APIKey != "" {
- transcriber = voice.NewGroqTranscriber(cfg.Providers.Groq.APIKey)
+ groqAPIKey := cfg.Providers.Groq.APIKey
+ if groqAPIKey == "" {
+ for _, mc := range cfg.ModelList {
+ if strings.HasPrefix(mc.Model, "groq/") && mc.APIKey != "" {
+ groqAPIKey = mc.APIKey
+ break
+ }
+ }
+ }
+ if groqAPIKey != "" {
+ transcriber = voice.NewGroqTranscriber(groqAPIKey)
logger.InfoC("voice", "Groq voice transcription enabled")
}
@@ -181,7 +199,7 @@ func gatewayCmd() {
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
go func() {
if err := healthServer.Start(); err != nil && err != http.ErrServerClosed {
- logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()})
+ logger.ErrorCF("health", "Health server error", map[string]any{"error": err.Error()})
}
}()
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
@@ -203,7 +221,14 @@ func gatewayCmd() {
fmt.Println("✓ Gateway stopped")
}
-func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, cfg *config.Config) *cron.CronService {
+func setupCronTool(
+ agentLoop *agent.AgentLoop,
+ msgBus *bus.MessageBus,
+ workspace string,
+ restrict bool,
+ execTimeout time.Duration,
+ cfg *config.Config,
+) *cron.CronService {
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
// Create cron service
diff --git a/cmd/picoclaw/cmd_onboard.go b/cmd/picoclaw/cmd_onboard.go
index 6e61e3267..1a9ebad61 100644
--- a/cmd/picoclaw/cmd_onboard.go
+++ b/cmd/picoclaw/cmd_onboard.go
@@ -55,7 +55,7 @@ func onboard() {
func copyEmbeddedToTarget(targetDir string) error {
// Ensure target directory exists
- if err := os.MkdirAll(targetDir, 0755); err != nil {
+ if err := os.MkdirAll(targetDir, 0o755); err != nil {
return fmt.Errorf("Failed to create target directory: %w", err)
}
@@ -85,12 +85,12 @@ func copyEmbeddedToTarget(targetDir string) error {
targetPath := filepath.Join(targetDir, new_path)
// Ensure target file's directory exists
- if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
}
// Write file
- if err := os.WriteFile(targetPath, data, 0644); err != nil {
+ if err := os.WriteFile(targetPath, data, 0o644); err != nil {
return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
}
diff --git a/cmd/picoclaw/cmd_skills.go b/cmd/picoclaw/cmd_skills.go
index 32b7c62b8..0814494b3 100644
--- a/cmd/picoclaw/cmd_skills.go
+++ b/cmd/picoclaw/cmd_skills.go
@@ -118,7 +118,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
workspace := cfg.WorkspacePath()
targetDir := filepath.Join(workspace, "skills", slug)
- if _, err := os.Stat(targetDir); err == nil {
+ if _, err = os.Stat(targetDir); err == nil {
fmt.Printf("\u2717 Skill '%s' already installed at %s\n", slug, targetDir)
os.Exit(1)
}
@@ -126,7 +126,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
- if err := os.MkdirAll(filepath.Join(workspace, "skills"), 0755); err != nil {
+ if err = os.MkdirAll(filepath.Join(workspace, "skills"), 0o755); err != nil {
fmt.Printf("\u2717 Failed to create skills directory: %v\n", err)
os.Exit(1)
}
@@ -193,7 +193,7 @@ func skillsInstallBuiltinCmd(workspace string) {
continue
}
- if err := os.MkdirAll(workspacePath, 0755); err != nil {
+ if err := os.MkdirAll(workspacePath, 0o755); err != nil {
fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err)
continue
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 32e8ee339..c268b01cd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,6 +10,9 @@ services:
container_name: picoclaw-agent
profiles:
- agent
+ # Uncomment to access host network; leave commented unless needed.
+ #extra_hosts:
+ # - "host.docker.internal:host-gateway"
volumes:
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
- picoclaw-workspace:/home/picoclaw/.picoclaw/workspace
@@ -29,6 +32,9 @@ services:
restart: unless-stopped
profiles:
- gateway
+ # Uncomment to access host network; leave commented unless needed.
+ #extra_hosts:
+ # - "host.docker.internal:host-gateway"
volumes:
# Configuration file
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
diff --git a/docs/ANTIGRAVITY_AUTH.md b/docs/ANTIGRAVITY_AUTH.md
index 5d68de427..89261d899 100644
--- a/docs/ANTIGRAVITY_AUTH.md
+++ b/docs/ANTIGRAVITY_AUTH.md
@@ -378,7 +378,7 @@ const antigravityPlugin = {
description: "OAuth flow for Google Antigravity (Cloud Code Assist)",
configSchema: emptyPluginConfigSchema(),
- register(api: OpenClawPluginApi) {
+ register(api: PicoClawPluginApi) {
api.registerProvider({
id: "google-antigravity",
label: "Google Antigravity",
@@ -405,7 +405,7 @@ const antigravityPlugin = {
```typescript
type ProviderAuthContext = {
- config: OpenClawConfig;
+ config: PicoClawConfig;
agentDir?: string;
workspaceDir?: string;
prompter: WizardPrompter; // UI prompts/notifications
@@ -426,7 +426,7 @@ type ProviderAuthResult = {
profileId: string;
credential: AuthProfileCredential;
}>;
- configPatch?: Partial%s", escaped))
+ text = strings.ReplaceAll(
+ text,
+ fmt.Sprintf("\x00CB%d\x00", i),
+ fmt.Sprintf("%s", escaped),
+ )
}
return text
diff --git a/pkg/channels/telegram_commands.go b/pkg/channels/telegram_commands.go
index df245e156..a084b641b 100644
--- a/pkg/channels/telegram_commands.go
+++ b/pkg/channels/telegram_commands.go
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/mymmrac/telego"
+
"github.com/sipeed/picoclaw/pkg/config"
)
@@ -35,6 +36,7 @@ func commandArgs(text string) string {
}
return strings.TrimSpace(parts[1])
}
+
func (c *cmd) Help(ctx context.Context, message telego.Message) error {
msg := `/start - Start the bot
/help - Show this help message
@@ -96,6 +98,7 @@ func (c *cmd) Show(ctx context.Context, message telego.Message) error {
})
return err
}
+
func (c *cmd) List(ctx context.Context, message telego.Message) error {
args := commandArgs(message.Text)
if args == "" {
diff --git a/pkg/channels/wecom.go b/pkg/channels/wecom.go
index 064568243..f8daf89de 100644
--- a/pkg/channels/wecom.go
+++ b/pkg/channels/wecom.go
@@ -134,7 +134,7 @@ func (c *WeComBotChannel) Start(ctx context.Context) error {
}
c.setRunning(true)
- logger.InfoCF("wecom", "WeCom Bot channel started", map[string]interface{}{
+ logger.InfoCF("wecom", "WeCom Bot channel started", map[string]any{
"address": addr,
"path": webhookPath,
})
@@ -142,7 +142,7 @@ func (c *WeComBotChannel) Start(ctx context.Context) error {
// Start server in goroutine
go func() {
if err := c.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- logger.ErrorCF("wecom", "HTTP server error", map[string]interface{}{
+ logger.ErrorCF("wecom", "HTTP server error", map[string]any{
"error": err.Error(),
})
}
@@ -178,7 +178,7 @@ func (c *WeComBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
return fmt.Errorf("wecom channel not running")
}
- logger.DebugCF("wecom", "Sending message via webhook", map[string]interface{}{
+ logger.DebugCF("wecom", "Sending message via webhook", map[string]any{
"chat_id": msg.ChatID,
"preview": utils.Truncate(msg.Content, 100),
})
@@ -230,7 +230,7 @@ func (c *WeComBotChannel) handleVerification(ctx context.Context, w http.Respons
// Reference: https://developer.work.weixin.qq.com/document/path/101033
decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, "")
if err != nil {
- logger.ErrorCF("wecom", "Failed to decrypt echostr", map[string]interface{}{
+ logger.ErrorCF("wecom", "Failed to decrypt echostr", map[string]any{
"error": err.Error(),
})
http.Error(w, "Decryption failed", http.StatusInternalServerError)
@@ -272,8 +272,8 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp
AgentID string `xml:"AgentID"`
}
- if err := xml.Unmarshal(body, &encryptedMsg); err != nil {
- logger.ErrorCF("wecom", "Failed to parse XML", map[string]interface{}{
+ if err = xml.Unmarshal(body, &encryptedMsg); err != nil {
+ logger.ErrorCF("wecom", "Failed to parse XML", map[string]any{
"error": err.Error(),
})
http.Error(w, "Invalid XML", http.StatusBadRequest)
@@ -292,7 +292,7 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp
// Reference: https://developer.work.weixin.qq.com/document/path/101033
decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, "")
if err != nil {
- logger.ErrorCF("wecom", "Failed to decrypt message", map[string]interface{}{
+ logger.ErrorCF("wecom", "Failed to decrypt message", map[string]any{
"error": err.Error(),
})
http.Error(w, "Decryption failed", http.StatusInternalServerError)
@@ -302,7 +302,7 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp
// Parse decrypted JSON message (AIBOT uses JSON format)
var msg WeComBotMessage
if err := json.Unmarshal([]byte(decryptedMsg), &msg); err != nil {
- logger.ErrorCF("wecom", "Failed to parse decrypted message", map[string]interface{}{
+ logger.ErrorCF("wecom", "Failed to parse decrypted message", map[string]any{
"error": err.Error(),
})
http.Error(w, "Invalid message format", http.StatusBadRequest)
@@ -320,8 +320,9 @@ func (c *WeComBotChannel) handleMessageCallback(ctx context.Context, w http.Resp
// processMessage processes the received message
func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessage) {
// Skip unsupported message types
- if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" && msg.MsgType != "file" && msg.MsgType != "mixed" {
- logger.DebugCF("wecom", "Skipping non-supported message type", map[string]interface{}{
+ if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" && msg.MsgType != "file" &&
+ msg.MsgType != "mixed" {
+ logger.DebugCF("wecom", "Skipping non-supported message type", map[string]any{
"msg_type": msg.MsgType,
})
return
@@ -332,7 +333,7 @@ func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessag
c.msgMu.Lock()
if c.processedMsgs[msgID] {
c.msgMu.Unlock()
- logger.DebugCF("wecom", "Skipping duplicate message", map[string]interface{}{
+ logger.DebugCF("wecom", "Skipping duplicate message", map[string]any{
"msg_id": msgID,
})
return
@@ -399,7 +400,7 @@ func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessag
metadata["sender_id"] = senderID
}
- logger.DebugCF("wecom", "Received message", map[string]interface{}{
+ logger.DebugCF("wecom", "Received message", map[string]any{
"sender_id": senderID,
"msg_type": msg.MsgType,
"peer_kind": peerKind,
@@ -468,7 +469,7 @@ func (c *WeComBotChannel) sendWebhookReply(ctx context.Context, userID, content
// handleHealth handles health check requests
func (c *WeComBotChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
- status := map[string]interface{}{
+ status := map[string]any{
"status": "ok",
"running": c.IsRunning(),
}
diff --git a/pkg/channels/wecom_app.go b/pkg/channels/wecom_app.go
index 63a1dd815..715c48707 100644
--- a/pkg/channels/wecom_app.go
+++ b/pkg/channels/wecom_app.go
@@ -145,7 +145,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error {
// Get initial access token
if err := c.refreshAccessToken(); err != nil {
- logger.WarnCF("wecom_app", "Failed to get initial access token", map[string]interface{}{
+ logger.WarnCF("wecom_app", "Failed to get initial access token", map[string]any{
"error": err.Error(),
})
}
@@ -171,7 +171,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error {
}
c.setRunning(true)
- logger.InfoCF("wecom_app", "WeCom App channel started", map[string]interface{}{
+ logger.InfoCF("wecom_app", "WeCom App channel started", map[string]any{
"address": addr,
"path": webhookPath,
})
@@ -179,7 +179,7 @@ func (c *WeComAppChannel) Start(ctx context.Context) error {
// Start server in goroutine
go func() {
if err := c.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- logger.ErrorCF("wecom_app", "HTTP server error", map[string]interface{}{
+ logger.ErrorCF("wecom_app", "HTTP server error", map[string]any{
"error": err.Error(),
})
}
@@ -218,7 +218,7 @@ func (c *WeComAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
return fmt.Errorf("no valid access token available")
}
- logger.DebugCF("wecom_app", "Sending message", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Sending message", map[string]any{
"chat_id": msg.ChatID,
"preview": utils.Truncate(msg.Content, 100),
})
@@ -231,7 +231,7 @@ func (c *WeComAppChannel) handleWebhook(w http.ResponseWriter, r *http.Request)
ctx := r.Context()
// Log all incoming requests for debugging
- logger.DebugCF("wecom_app", "Received webhook request", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Received webhook request", map[string]any{
"method": r.Method,
"url": r.URL.String(),
"path": r.URL.Path,
@@ -250,7 +250,7 @@ func (c *WeComAppChannel) handleWebhook(w http.ResponseWriter, r *http.Request)
return
}
- logger.WarnCF("wecom_app", "Method not allowed", map[string]interface{}{
+ logger.WarnCF("wecom_app", "Method not allowed", map[string]any{
"method": r.Method,
})
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -264,7 +264,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons
nonce := query.Get("nonce")
echostr := query.Get("echostr")
- logger.DebugCF("wecom_app", "Handling verification request", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Handling verification request", map[string]any{
"msg_signature": msgSignature,
"timestamp": timestamp,
"nonce": nonce,
@@ -280,7 +280,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons
// Verify signature
if !WeComVerifySignature(c.config.Token, msgSignature, timestamp, nonce, echostr) {
- logger.WarnCF("wecom_app", "Signature verification failed", map[string]interface{}{
+ logger.WarnCF("wecom_app", "Signature verification failed", map[string]any{
"token": c.config.Token,
"msg_signature": msgSignature,
"timestamp": timestamp,
@@ -294,13 +294,13 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons
// Decrypt echostr with CorpID verification
// For WeCom App (自建应用), receiveid should be corp_id
- logger.DebugCF("wecom_app", "Attempting to decrypt echostr", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Attempting to decrypt echostr", map[string]any{
"encoding_aes_key": c.config.EncodingAESKey,
"corp_id": c.config.CorpID,
})
decryptedEchoStr, err := WeComDecryptMessageWithVerify(echostr, c.config.EncodingAESKey, c.config.CorpID)
if err != nil {
- logger.ErrorCF("wecom_app", "Failed to decrypt echostr", map[string]interface{}{
+ logger.ErrorCF("wecom_app", "Failed to decrypt echostr", map[string]any{
"error": err.Error(),
"encoding_aes_key": c.config.EncodingAESKey,
"corp_id": c.config.CorpID,
@@ -309,7 +309,7 @@ func (c *WeComAppChannel) handleVerification(ctx context.Context, w http.Respons
return
}
- logger.DebugCF("wecom_app", "Successfully decrypted echostr", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Successfully decrypted echostr", map[string]any{
"decrypted": decryptedEchoStr,
})
@@ -348,8 +348,8 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp
AgentID string `xml:"AgentID"`
}
- if err := xml.Unmarshal(body, &encryptedMsg); err != nil {
- logger.ErrorCF("wecom_app", "Failed to parse XML", map[string]interface{}{
+ if err = xml.Unmarshal(body, &encryptedMsg); err != nil {
+ logger.ErrorCF("wecom_app", "Failed to parse XML", map[string]any{
"error": err.Error(),
})
http.Error(w, "Invalid XML", http.StatusBadRequest)
@@ -367,7 +367,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp
// For WeCom App (自建应用), receiveid should be corp_id
decryptedMsg, err := WeComDecryptMessageWithVerify(encryptedMsg.Encrypt, c.config.EncodingAESKey, c.config.CorpID)
if err != nil {
- logger.ErrorCF("wecom_app", "Failed to decrypt message", map[string]interface{}{
+ logger.ErrorCF("wecom_app", "Failed to decrypt message", map[string]any{
"error": err.Error(),
})
http.Error(w, "Decryption failed", http.StatusInternalServerError)
@@ -377,7 +377,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp
// Parse decrypted XML message
var msg WeComXMLMessage
if err := xml.Unmarshal([]byte(decryptedMsg), &msg); err != nil {
- logger.ErrorCF("wecom_app", "Failed to parse decrypted message", map[string]interface{}{
+ logger.ErrorCF("wecom_app", "Failed to parse decrypted message", map[string]any{
"error": err.Error(),
})
http.Error(w, "Invalid message format", http.StatusBadRequest)
@@ -396,7 +396,7 @@ func (c *WeComAppChannel) handleMessageCallback(ctx context.Context, w http.Resp
func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessage) {
// Skip non-text messages for now (can be extended)
if msg.MsgType != "text" && msg.MsgType != "image" && msg.MsgType != "voice" {
- logger.DebugCF("wecom_app", "Skipping non-supported message type", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Skipping non-supported message type", map[string]any{
"msg_type": msg.MsgType,
})
return
@@ -408,7 +408,7 @@ func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessag
c.msgMu.Lock()
if c.processedMsgs[msgID] {
c.msgMu.Unlock()
- logger.DebugCF("wecom_app", "Skipping duplicate message", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Skipping duplicate message", map[string]any{
"msg_id": msgID,
})
return
@@ -441,7 +441,7 @@ func (c *WeComAppChannel) processMessage(ctx context.Context, msg WeComXMLMessag
content := msg.Content
- logger.DebugCF("wecom_app", "Received message", map[string]interface{}{
+ logger.DebugCF("wecom_app", "Received message", map[string]any{
"sender_id": senderID,
"msg_type": msg.MsgType,
"preview": utils.Truncate(content, 50),
@@ -462,7 +462,7 @@ func (c *WeComAppChannel) tokenRefreshLoop() {
return
case <-ticker.C:
if err := c.refreshAccessToken(); err != nil {
- logger.ErrorCF("wecom_app", "Failed to refresh access token", map[string]interface{}{
+ logger.ErrorCF("wecom_app", "Failed to refresh access token", map[string]any{
"error": err.Error(),
})
}
@@ -628,7 +628,7 @@ func (c *WeComAppChannel) sendMarkdownMessage(ctx context.Context, accessToken,
// handleHealth handles health check requests
func (c *WeComAppChannel) handleHealth(w http.ResponseWriter, r *http.Request) {
- status := map[string]interface{}{
+ status := map[string]any{
"status": "ok",
"running": c.IsRunning(),
"has_token": c.getAccessToken() != "",
diff --git a/pkg/channels/wecom_app_test.go b/pkg/channels/wecom_app_test.go
index bc40806bb..abf15c52b 100644
--- a/pkg/channels/wecom_app_test.go
+++ b/pkg/channels/wecom_app_test.go
@@ -399,7 +399,11 @@ func TestWeComAppHandleVerification(t *testing.T) {
nonce := "test_nonce"
signature := generateSignatureApp("test_token", timestamp, nonce, encryptedEchostr)
- req := httptest.NewRequest(http.MethodGet, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil)
+ req := httptest.NewRequest(
+ http.MethodGet,
+ "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr,
+ nil,
+ )
w := httptest.NewRecorder()
ch.handleVerification(context.Background(), w, req)
@@ -429,7 +433,11 @@ func TestWeComAppHandleVerification(t *testing.T) {
timestamp := "1234567890"
nonce := "test_nonce"
- req := httptest.NewRequest(http.MethodGet, "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil)
+ req := httptest.NewRequest(
+ http.MethodGet,
+ "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr,
+ nil,
+ )
w := httptest.NewRecorder()
ch.handleVerification(context.Background(), w, req)
@@ -481,7 +489,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) {
nonce := "test_nonce"
signature := generateSignatureApp("test_token", timestamp, nonce, encrypted)
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce,
+ bytes.NewReader(wrapperData),
+ )
w := httptest.NewRecorder()
ch.handleMessageCallback(context.Background(), w, req)
@@ -510,7 +522,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) {
nonce := "test_nonce"
signature := generateSignatureApp("test_token", timestamp, nonce, "")
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, strings.NewReader("invalid xml"))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce,
+ strings.NewReader("invalid xml"),
+ )
w := httptest.NewRecorder()
ch.handleMessageCallback(context.Background(), w, req)
@@ -532,7 +548,11 @@ func TestWeComAppHandleMessageCallback(t *testing.T) {
timestamp := "1234567890"
nonce := "test_nonce"
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom-app?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce,
+ bytes.NewReader(wrapperData),
+ )
w := httptest.NewRecorder()
ch.handleMessageCallback(context.Background(), w, req)
@@ -646,7 +666,11 @@ func TestWeComAppHandleWebhook(t *testing.T) {
nonce := "test_nonce"
signature := generateSignatureApp("test_token", timestamp, nonce, encoded)
- req := httptest.NewRequest(http.MethodGet, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded, nil)
+ req := httptest.NewRequest(
+ http.MethodGet,
+ "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded,
+ nil,
+ )
w := httptest.NewRecorder()
ch.handleWebhook(w, req)
@@ -669,7 +693,11 @@ func TestWeComAppHandleWebhook(t *testing.T) {
nonce := "test_nonce"
signature := generateSignatureApp("test_token", timestamp, nonce, encryptedWrapper.Encrypt)
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom-app?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce,
+ bytes.NewReader(wrapperData),
+ )
w := httptest.NewRecorder()
ch.handleWebhook(w, req)
@@ -824,19 +852,6 @@ func TestWeComAppMessageStructures(t *testing.T) {
}
})
- t.Run("WeComImageMessage structure", func(t *testing.T) {
- msg := WeComImageMessage{
- ToUser: "user123",
- MsgType: "image",
- AgentID: 1000002,
- }
- msg.Image.MediaID = "media_123456"
-
- if msg.Image.MediaID != "media_123456" {
- t.Errorf("Image.MediaID = %q, want %q", msg.Image.MediaID, "media_123456")
- }
- })
-
t.Run("WeComAccessTokenResponse structure", func(t *testing.T) {
jsonData := `{
"errcode": 0,
diff --git a/pkg/channels/wecom_test.go b/pkg/channels/wecom_test.go
index c3f889c64..8afa7e8c3 100644
--- a/pkg/channels/wecom_test.go
+++ b/pkg/channels/wecom_test.go
@@ -198,10 +198,8 @@ func TestWeComBotVerifySignature(t *testing.T) {
Token: "",
WebhookURL: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test",
}
- base := NewBaseChannel("wecom", cfgEmpty, msgBus, cfgEmpty.AllowFrom)
chEmpty := &WeComBotChannel{
- BaseChannel: base,
- config: cfgEmpty,
+ config: cfgEmpty,
}
if !WeComVerifySignature(chEmpty.config.Token, "any_sig", "any_ts", "any_nonce", "any_msg") {
@@ -358,7 +356,11 @@ func TestWeComBotHandleVerification(t *testing.T) {
nonce := "test_nonce"
signature := generateSignature("test_token", timestamp, nonce, encryptedEchostr)
- req := httptest.NewRequest(http.MethodGet, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil)
+ req := httptest.NewRequest(
+ http.MethodGet,
+ "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr,
+ nil,
+ )
w := httptest.NewRecorder()
ch.handleVerification(context.Background(), w, req)
@@ -388,7 +390,11 @@ func TestWeComBotHandleVerification(t *testing.T) {
timestamp := "1234567890"
nonce := "test_nonce"
- req := httptest.NewRequest(http.MethodGet, "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr, nil)
+ req := httptest.NewRequest(
+ http.MethodGet,
+ "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encryptedEchostr,
+ nil,
+ )
w := httptest.NewRecorder()
ch.handleVerification(context.Background(), w, req)
@@ -437,7 +443,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
nonce := "test_nonce"
signature := generateSignature("test_token", timestamp, nonce, encrypted)
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce,
+ bytes.NewReader(wrapperData),
+ )
w := httptest.NewRecorder()
ch.handleMessageCallback(context.Background(), w, req)
@@ -479,7 +489,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
nonce := "test_nonce"
signature := generateSignature("test_token", timestamp, nonce, encrypted)
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce,
+ bytes.NewReader(wrapperData),
+ )
w := httptest.NewRecorder()
ch.handleMessageCallback(context.Background(), w, req)
@@ -508,7 +522,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
nonce := "test_nonce"
signature := generateSignature("test_token", timestamp, nonce, "")
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, strings.NewReader("invalid xml"))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce,
+ strings.NewReader("invalid xml"),
+ )
w := httptest.NewRecorder()
ch.handleMessageCallback(context.Background(), w, req)
@@ -530,7 +548,11 @@ func TestWeComBotHandleMessageCallback(t *testing.T) {
timestamp := "1234567890"
nonce := "test_nonce"
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom?msg_signature=invalid_sig×tamp="+timestamp+"&nonce="+nonce,
+ bytes.NewReader(wrapperData),
+ )
w := httptest.NewRecorder()
ch.handleMessageCallback(context.Background(), w, req)
@@ -625,7 +647,11 @@ func TestWeComBotHandleWebhook(t *testing.T) {
nonce := "test_nonce"
signature := generateSignature("test_token", timestamp, nonce, encoded)
- req := httptest.NewRequest(http.MethodGet, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded, nil)
+ req := httptest.NewRequest(
+ http.MethodGet,
+ "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce+"&echostr="+encoded,
+ nil,
+ )
w := httptest.NewRecorder()
ch.handleWebhook(w, req)
@@ -648,7 +674,11 @@ func TestWeComBotHandleWebhook(t *testing.T) {
nonce := "test_nonce"
signature := generateSignature("test_token", timestamp, nonce, encryptedWrapper.Encrypt)
- req := httptest.NewRequest(http.MethodPost, "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce, bytes.NewReader(wrapperData))
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/webhook/wecom?msg_signature="+signature+"×tamp="+timestamp+"&nonce="+nonce,
+ bytes.NewReader(wrapperData),
+ )
w := httptest.NewRecorder()
ch.handleWebhook(w, req)
diff --git a/pkg/channels/whatsapp.go b/pkg/channels/whatsapp.go
index 065424e0c..958d850bb 100644
--- a/pkg/channels/whatsapp.go
+++ b/pkg/channels/whatsapp.go
@@ -86,7 +86,7 @@ func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
return fmt.Errorf("whatsapp connection not established")
}
- payload := map[string]interface{}{
+ payload := map[string]any{
"type": "message",
"to": msg.ChatID,
"content": msg.Content,
@@ -126,7 +126,7 @@ func (c *WhatsAppChannel) listen(ctx context.Context) {
continue
}
- var msg map[string]interface{}
+ var msg map[string]any
if err := json.Unmarshal(message, &msg); err != nil {
log.Printf("Failed to unmarshal WhatsApp message: %v", err)
continue
@@ -144,7 +144,7 @@ func (c *WhatsAppChannel) listen(ctx context.Context) {
}
}
-func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]interface{}) {
+func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]any) {
senderID, ok := msg["from"].(string)
if !ok {
return
@@ -161,7 +161,7 @@ func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]interface{}) {
}
var mediaPaths []string
- if mediaData, ok := msg["media"].([]interface{}); ok {
+ if mediaData, ok := msg["media"].([]any); ok {
mediaPaths = make([]string, 0, len(mediaData))
for _, m := range mediaData {
if path, ok := m.(string); ok {
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 005631e4a..20556011a 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -26,7 +26,7 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
}
// Try []interface{} to handle mixed types
- var raw []interface{}
+ var raw []any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
@@ -167,16 +167,16 @@ type SessionConfig struct {
}
type AgentDefaults struct {
- Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
- RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
- Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
- Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
+ Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
+ RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
+ Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
+ Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
- ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
+ ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
- MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
- Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
- MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
+ MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
+ Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
+ MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
}
type ChannelsConfig struct {
@@ -195,114 +195,114 @@ type ChannelsConfig struct {
}
type WhatsAppConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
}
type TelegramConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
- Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
- Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
+ Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
+ Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
}
type FeishuConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
- AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
- AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
- EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
+ AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
+ AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"`
+ EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
}
type DiscordConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
- Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
+ Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"`
}
type MaixCamConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
- Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
- Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
+ Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
+ Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
}
type QQConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
- AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
+ AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
}
type DingTalkConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
- ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
+ ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
}
type SlackConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
- BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
- AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
+ BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
+ AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
}
type LINEConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
- ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
+ ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
- WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
- WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
- WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
+ WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
+ WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
+ WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
}
type OneBotConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
- WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
- AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
- ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
+ WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
+ AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
+ ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
}
type WeComConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"`
- Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"`
+ Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"`
EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_ENCODING_AES_KEY"`
- WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"`
- WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"`
- WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"`
- WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"`
- ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"`
+ WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"`
+ WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"`
+ WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"`
+ WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"`
+ ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"`
}
type WeComAppConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"`
- CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"`
- CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"`
- AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"`
- Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"`
+ CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"`
+ CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"`
+ AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"`
+ Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"`
EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_APP_ENCODING_AES_KEY"`
- WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"`
- WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"`
- WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"`
- AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"`
- ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"`
+ WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"`
+ WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"`
+ WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"`
+ ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"`
}
type HeartbeatConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
}
type DevicesConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"`
MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
}
@@ -359,11 +359,11 @@ func (p ProvidersConfig) MarshalJSON() ([]byte, error) {
}
type ProviderConfig struct {
- APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
- APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
- Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
- AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
- ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
+ APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
+ APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
+ Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
+ AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
+ ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc`
}
type OpenAIProviderConfig struct {
@@ -413,19 +413,19 @@ type GatewayConfig struct {
}
type BraveConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
- APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
+ APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
}
type DuckDuckGoConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
}
type PerplexityConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
- APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
+ APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
}
@@ -458,7 +458,7 @@ type SkillsToolsConfig struct {
}
type SearchCacheConfig struct {
- MaxSize int `json:"max_size" env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"`
+ MaxSize int `json:"max_size" env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"`
TTLSeconds int `json:"ttl_seconds" env:"PICOCLAW_SKILLS_SEARCH_CACHE_TTL_SECONDS"`
}
@@ -467,14 +467,14 @@ type SkillsRegistriesConfig struct {
}
type ClawHubRegistryConfig struct {
- Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"`
- BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"`
- AuthToken string `json:"auth_token" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_AUTH_TOKEN"`
- SearchPath string `json:"search_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"`
- SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"`
- DownloadPath string `json:"download_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"`
- Timeout int `json:"timeout" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"`
- MaxZipSize int `json:"max_zip_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"`
+ Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"`
+ BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"`
+ AuthToken string `json:"auth_token" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_AUTH_TOKEN"`
+ SearchPath string `json:"search_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"`
+ SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"`
+ DownloadPath string `json:"download_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"`
+ Timeout int `json:"timeout" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"`
+ MaxZipSize int `json:"max_zip_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"`
MaxResponseSize int `json:"max_response_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_RESPONSE_SIZE"`
}
@@ -517,11 +517,11 @@ func SaveConfig(path string, cfg *Config) error {
}
dir := filepath.Dir(path)
- if err := os.MkdirAll(dir, 0755); err != nil {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
- return os.WriteFile(path, data, 0600)
+ return os.WriteFile(path, data, 0o600)
}
func (c *Config) WorkspacePath() string {
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 672b1abdc..f88c0269c 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -55,7 +55,7 @@ func TestAgentModelConfig_MarshalObject(t *testing.T) {
if err != nil {
t.Fatalf("marshal: %v", err)
}
- var result map[string]interface{}
+ var result map[string]any
json.Unmarshal(data, &result)
if result["primary"] != "claude-opus" {
t.Errorf("primary = %v", result["primary"])
@@ -319,7 +319,7 @@ func TestSaveConfig_FilePermissions(t *testing.T) {
}
perm := info.Mode().Perm()
- if perm != 0600 {
+ if perm != 0o600 {
t.Errorf("config file has permission %04o, want 0600", perm)
}
}
diff --git a/pkg/config/migration_test.go b/pkg/config/migration_test.go
index b9a333f9e..1e8139e68 100644
--- a/pkg/config/migration_test.go
+++ b/pkg/config/migration_test.go
@@ -361,7 +361,10 @@ func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) {
Agents: AgentsConfig{
Defaults: AgentDefaults{
Provider: tt.providerAlias,
- Model: strings.TrimPrefix(tt.expectedModel, tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1]),
+ Model: strings.TrimPrefix(
+ tt.expectedModel,
+ tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1],
+ ),
},
},
Providers: ProvidersConfig{},
@@ -382,7 +385,10 @@ func TestConvertProvidersToModelList_ProviderNameAliases(t *testing.T) {
}
// Need to fix the model name in config
- cfg.Agents.Defaults.Model = strings.TrimPrefix(tt.expectedModel, tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1])
+ cfg.Agents.Defaults.Model = strings.TrimPrefix(
+ tt.expectedModel,
+ tt.expectedModel[:strings.Index(tt.expectedModel, "/")+1],
+ )
result := ConvertProvidersToModelList(cfg)
if len(result) != 1 {
@@ -515,7 +521,11 @@ func TestBuildModelWithProtocol_AlreadyHasPrefix(t *testing.T) {
func TestBuildModelWithProtocol_DifferentPrefix(t *testing.T) {
result := buildModelWithProtocol("anthropic", "openrouter/claude-sonnet-4.6")
if result != "openrouter/claude-sonnet-4.6" {
- t.Errorf("buildModelWithProtocol(anthropic, openrouter/claude-sonnet-4.6) = %q, want %q", result, "openrouter/claude-sonnet-4.6")
+ t.Errorf(
+ "buildModelWithProtocol(anthropic, openrouter/claude-sonnet-4.6) = %q, want %q",
+ result,
+ "openrouter/claude-sonnet-4.6",
+ )
}
}
diff --git a/pkg/cron/service.go b/pkg/cron/service.go
index 9f62c743b..e699a44b5 100644
--- a/pkg/cron/service.go
+++ b/pkg/cron/service.go
@@ -331,7 +331,7 @@ func (cs *CronService) loadStore() error {
func (cs *CronService) saveStoreUnsafe() error {
dir := filepath.Dir(cs.storePath)
- if err := os.MkdirAll(dir, 0755); err != nil {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
@@ -340,10 +340,16 @@ func (cs *CronService) saveStoreUnsafe() error {
return err
}
- return os.WriteFile(cs.storePath, data, 0600)
+ return os.WriteFile(cs.storePath, data, 0o600)
}
-func (cs *CronService) AddJob(name string, schedule CronSchedule, message string, deliver bool, channel, to string) (*CronJob, error) {
+func (cs *CronService) AddJob(
+ name string,
+ schedule CronSchedule,
+ message string,
+ deliver bool,
+ channel, to string,
+) (*CronJob, error) {
cs.mu.Lock()
defer cs.mu.Unlock()
@@ -465,7 +471,7 @@ func (cs *CronService) ListJobs(includeDisabled bool) []CronJob {
return enabled
}
-func (cs *CronService) Status() map[string]interface{} {
+func (cs *CronService) Status() map[string]any {
cs.mu.RLock()
defer cs.mu.RUnlock()
@@ -476,7 +482,7 @@ func (cs *CronService) Status() map[string]interface{} {
}
}
- return map[string]interface{}{
+ return map[string]any{
"enabled": cs.running,
"jobs": len(cs.store.Jobs),
"nextWakeAtMS": cs.getNextWakeMS(),
diff --git a/pkg/cron/service_test.go b/pkg/cron/service_test.go
index 53d69f6a9..1a0dd1829 100644
--- a/pkg/cron/service_test.go
+++ b/pkg/cron/service_test.go
@@ -28,7 +28,7 @@ func TestSaveStore_FilePermissions(t *testing.T) {
}
perm := info.Mode().Perm()
- if perm != 0600 {
+ if perm != 0o600 {
t.Errorf("cron store has permission %04o, want 0600", perm)
}
}
diff --git a/pkg/devices/service.go b/pkg/devices/service.go
index 05a254729..1541d3c57 100644
--- a/pkg/devices/service.go
+++ b/pkg/devices/service.go
@@ -63,14 +63,14 @@ func (s *Service) Start(ctx context.Context) error {
for _, src := range s.sources {
eventCh, err := src.Start(s.ctx)
if err != nil {
- logger.ErrorCF("devices", "Failed to start source", map[string]interface{}{
+ logger.ErrorCF("devices", "Failed to start source", map[string]any{
"kind": src.Kind(),
"error": err.Error(),
})
continue
}
go s.handleEvents(src.Kind(), eventCh)
- logger.InfoCF("devices", "Device source started", map[string]interface{}{
+ logger.InfoCF("devices", "Device source started", map[string]any{
"kind": src.Kind(),
})
}
@@ -115,7 +115,7 @@ func (s *Service) sendNotification(ev *events.DeviceEvent) {
lastChannel := s.state.GetLastChannel()
if lastChannel == "" {
- logger.DebugCF("devices", "No last channel, skipping notification", map[string]interface{}{
+ logger.DebugCF("devices", "No last channel, skipping notification", map[string]any{
"event": ev.FormatMessage(),
})
return
@@ -133,7 +133,7 @@ func (s *Service) sendNotification(ev *events.DeviceEvent) {
Content: msg,
})
- logger.InfoCF("devices", "Device notification sent", map[string]interface{}{
+ logger.InfoCF("devices", "Device notification sent", map[string]any{
"kind": ev.Kind,
"action": ev.Action,
"to": platform,
diff --git a/pkg/devices/sources/usb_linux.go b/pkg/devices/sources/usb_linux.go
index 1f6c068b3..be0193cfb 100644
--- a/pkg/devices/sources/usb_linux.go
+++ b/pkg/devices/sources/usb_linux.go
@@ -115,7 +115,7 @@ func (m *USBMonitor) Start(ctx context.Context) (<-chan *events.DeviceEvent, err
}
if err := scanner.Err(); err != nil {
- logger.ErrorCF("devices", "udevadm scan error", map[string]interface{}{"error": err.Error()})
+ logger.ErrorCF("devices", "udevadm scan error", map[string]any{"error": err.Error()})
}
cmd.Wait()
}()
diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go
index dfdaef58b..75d6248b9 100644
--- a/pkg/heartbeat/service.go
+++ b/pkg/heartbeat/service.go
@@ -193,7 +193,7 @@ func (hs *HeartbeatService) executeHeartbeat() {
if result.Async {
hs.logInfo("Async task started: %s", result.ForLLM)
logger.InfoCF("heartbeat", "Async heartbeat task started",
- map[string]interface{}{
+ map[string]any{
"message": result.ForLLM,
})
return
@@ -275,7 +275,7 @@ This file contains tasks for the heartbeat service to check periodically.
Add your heartbeat tasks below this line:
`
- if err := os.WriteFile(heartbeatPath, []byte(defaultContent), 0644); err != nil {
+ if err := os.WriteFile(heartbeatPath, []byte(defaultContent), 0o644); err != nil {
hs.logError("Failed to create default HEARTBEAT.md: %v", err)
} else {
hs.logInfo("Created default HEARTBEAT.md template")
@@ -354,7 +354,7 @@ func (hs *HeartbeatService) logError(format string, args ...any) {
// log writes a message to the heartbeat log file
func (hs *HeartbeatService) log(level, format string, args ...any) {
logFile := filepath.Join(hs.workspace, "heartbeat.log")
- f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return
}
diff --git a/pkg/heartbeat/service_test.go b/pkg/heartbeat/service_test.go
index a2b59e350..a4dfa7a72 100644
--- a/pkg/heartbeat/service_test.go
+++ b/pkg/heartbeat/service_test.go
@@ -37,7 +37,7 @@ func TestExecuteHeartbeat_Async(t *testing.T) {
})
// Create HEARTBEAT.md
- os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
+ os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
// Execute heartbeat directly (internal method for testing)
hs.executeHeartbeat()
@@ -68,7 +68,7 @@ func TestExecuteHeartbeat_Error(t *testing.T) {
})
// Create HEARTBEAT.md
- os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
+ os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
hs.executeHeartbeat()
@@ -106,7 +106,7 @@ func TestExecuteHeartbeat_Silent(t *testing.T) {
})
// Create HEARTBEAT.md
- os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
+ os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
hs.executeHeartbeat()
@@ -174,7 +174,7 @@ func TestExecuteHeartbeat_NilResult(t *testing.T) {
})
// Create HEARTBEAT.md
- os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0644)
+ os.WriteFile(filepath.Join(tmpDir, "HEARTBEAT.md"), []byte("Test task"), 0o644)
// Should not panic with nil result
hs.executeHeartbeat()
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index 22f66829f..54de66bf9 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -41,12 +41,12 @@ type Logger struct {
}
type LogEntry struct {
- Level string `json:"level"`
- Timestamp string `json:"timestamp"`
- Component string `json:"component,omitempty"`
- Message string `json:"message"`
- Fields map[string]interface{} `json:"fields,omitempty"`
- Caller string `json:"caller,omitempty"`
+ Level string `json:"level"`
+ Timestamp string `json:"timestamp"`
+ Component string `json:"component,omitempty"`
+ Message string `json:"message"`
+ Fields map[string]any `json:"fields,omitempty"`
+ Caller string `json:"caller,omitempty"`
}
func init() {
@@ -71,7 +71,7 @@ func EnableFileLogging(filePath string) error {
mu.Lock()
defer mu.Unlock()
- file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
+ file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return fmt.Errorf("failed to open log file: %w", err)
}
@@ -96,7 +96,7 @@ func DisableFileLogging() {
}
}
-func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) {
+func logMessage(level LogLevel, component string, message string, fields map[string]any) {
if level < currentLevel {
return
}
@@ -150,7 +150,7 @@ func formatComponent(component string) string {
return fmt.Sprintf(" %s:", component)
}
-func formatFields(fields map[string]interface{}) string {
+func formatFields(fields map[string]any) string {
var parts []string
for k, v := range fields {
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
@@ -166,11 +166,11 @@ func DebugC(component string, message string) {
logMessage(DEBUG, component, message, nil)
}
-func DebugF(message string, fields map[string]interface{}) {
+func DebugF(message string, fields map[string]any) {
logMessage(DEBUG, "", message, fields)
}
-func DebugCF(component string, message string, fields map[string]interface{}) {
+func DebugCF(component string, message string, fields map[string]any) {
logMessage(DEBUG, component, message, fields)
}
@@ -182,11 +182,11 @@ func InfoC(component string, message string) {
logMessage(INFO, component, message, nil)
}
-func InfoF(message string, fields map[string]interface{}) {
+func InfoF(message string, fields map[string]any) {
logMessage(INFO, "", message, fields)
}
-func InfoCF(component string, message string, fields map[string]interface{}) {
+func InfoCF(component string, message string, fields map[string]any) {
logMessage(INFO, component, message, fields)
}
@@ -198,11 +198,11 @@ func WarnC(component string, message string) {
logMessage(WARN, component, message, nil)
}
-func WarnF(message string, fields map[string]interface{}) {
+func WarnF(message string, fields map[string]any) {
logMessage(WARN, "", message, fields)
}
-func WarnCF(component string, message string, fields map[string]interface{}) {
+func WarnCF(component string, message string, fields map[string]any) {
logMessage(WARN, component, message, fields)
}
@@ -214,11 +214,11 @@ func ErrorC(component string, message string) {
logMessage(ERROR, component, message, nil)
}
-func ErrorF(message string, fields map[string]interface{}) {
+func ErrorF(message string, fields map[string]any) {
logMessage(ERROR, "", message, fields)
}
-func ErrorCF(component string, message string, fields map[string]interface{}) {
+func ErrorCF(component string, message string, fields map[string]any) {
logMessage(ERROR, component, message, fields)
}
@@ -230,10 +230,10 @@ func FatalC(component string, message string) {
logMessage(FATAL, component, message, nil)
}
-func FatalF(message string, fields map[string]interface{}) {
+func FatalF(message string, fields map[string]any) {
logMessage(FATAL, "", message, fields)
}
-func FatalCF(component string, message string, fields map[string]interface{}) {
+func FatalCF(component string, message string, fields map[string]any) {
logMessage(FATAL, component, message, fields)
}
diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go
index 9b9c96820..6e6f8dfa8 100644
--- a/pkg/logger/logger_test.go
+++ b/pkg/logger/logger_test.go
@@ -54,11 +54,11 @@ func TestLoggerWithComponent(t *testing.T) {
name string
component string
message string
- fields map[string]interface{}
+ fields map[string]any
}{
{"Simple message", "test", "Hello, world!", nil},
{"Message with component", "discord", "Discord message", nil},
- {"Message with fields", "telegram", "Telegram message", map[string]interface{}{
+ {"Message with fields", "telegram", "Telegram message", map[string]any{
"user_id": "12345",
"count": 42,
}},
@@ -128,12 +128,12 @@ func TestLoggerHelperFunctions(t *testing.T) {
Error("This should log")
InfoC("test", "Component message")
- InfoF("Fields message", map[string]interface{}{"key": "value"})
+ InfoF("Fields message", map[string]any{"key": "value"})
WarnC("test", "Warning with component")
- ErrorF("Error with fields", map[string]interface{}{"error": "test"})
+ ErrorF("Error with fields", map[string]any{"error": "test"})
SetLevel(DEBUG)
DebugC("test", "Debug with component")
- WarnF("Warning with fields", map[string]interface{}{"key": "value"})
+ WarnF("Warning with fields", map[string]any{"key": "value"})
}
diff --git a/pkg/migrate/config.go b/pkg/migrate/config.go
index b01bb80e3..2237a1429 100644
--- a/pkg/migrate/config.go
+++ b/pkg/migrate/config.go
@@ -47,26 +47,26 @@ func findOpenClawConfig(openclawHome string) (string, error) {
return "", fmt.Errorf("no config file found in %s (tried openclaw.json, config.json)", openclawHome)
}
-func LoadOpenClawConfig(configPath string) (map[string]interface{}, error) {
+func LoadOpenClawConfig(configPath string) (map[string]any, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("reading OpenClaw config: %w", err)
}
- var raw map[string]interface{}
+ var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("parsing OpenClaw config: %w", err)
}
converted := convertKeysToSnake(raw)
- result, ok := converted.(map[string]interface{})
+ result, ok := converted.(map[string]any)
if !ok {
return nil, fmt.Errorf("unexpected config format")
}
return result, nil
}
-func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error) {
+func ConvertConfig(data map[string]any) (*config.Config, []string, error) {
cfg := config.DefaultConfig()
var warnings []string
@@ -92,7 +92,7 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
if providers, ok := getMap(data, "providers"); ok {
for name, val := range providers {
- pMap, ok := val.(map[string]interface{})
+ pMap, ok := val.(map[string]any)
if !ok {
continue
}
@@ -131,7 +131,7 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
if channels, ok := getMap(data, "channels"); ok {
for name, val := range channels {
- cMap, ok := val.(map[string]interface{})
+ cMap, ok := val.(map[string]any)
if !ok {
continue
}
@@ -318,16 +318,16 @@ func camelToSnake(s string) string {
return result.String()
}
-func convertKeysToSnake(data interface{}) interface{} {
+func convertKeysToSnake(data any) any {
switch v := data.(type) {
- case map[string]interface{}:
- result := make(map[string]interface{}, len(v))
+ case map[string]any:
+ result := make(map[string]any, len(v))
for key, val := range v {
result[camelToSnake(key)] = convertKeysToSnake(val)
}
return result
- case []interface{}:
- result := make([]interface{}, len(v))
+ case []any:
+ result := make([]any, len(v))
for i, val := range v {
result[i] = convertKeysToSnake(val)
}
@@ -342,16 +342,16 @@ func rewriteWorkspacePath(path string) string {
return path
}
-func getMap(data map[string]interface{}, key string) (map[string]interface{}, bool) {
+func getMap(data map[string]any, key string) (map[string]any, bool) {
v, ok := data[key]
if !ok {
return nil, false
}
- m, ok := v.(map[string]interface{})
+ m, ok := v.(map[string]any)
return m, ok
}
-func getString(data map[string]interface{}, key string) (string, bool) {
+func getString(data map[string]any, key string) (string, bool) {
v, ok := data[key]
if !ok {
return "", false
@@ -360,7 +360,7 @@ func getString(data map[string]interface{}, key string) (string, bool) {
return s, ok
}
-func getFloat(data map[string]interface{}, key string) (float64, bool) {
+func getFloat(data map[string]any, key string) (float64, bool) {
v, ok := data[key]
if !ok {
return 0, false
@@ -369,7 +369,7 @@ func getFloat(data map[string]interface{}, key string) (float64, bool) {
return f, ok
}
-func getBool(data map[string]interface{}, key string) (bool, bool) {
+func getBool(data map[string]any, key string) (bool, bool) {
v, ok := data[key]
if !ok {
return false, false
@@ -378,19 +378,19 @@ func getBool(data map[string]interface{}, key string) (bool, bool) {
return b, ok
}
-func getBoolOrDefault(data map[string]interface{}, key string, defaultVal bool) bool {
+func getBoolOrDefault(data map[string]any, key string, defaultVal bool) bool {
if v, ok := getBool(data, key); ok {
return v
}
return defaultVal
}
-func getStringSlice(data map[string]interface{}, key string) []string {
+func getStringSlice(data map[string]any, key string) []string {
v, ok := data[key]
if !ok {
return []string{}
}
- arr, ok := v.([]interface{})
+ arr, ok := v.([]any)
if !ok {
return []string{}
}
diff --git a/pkg/migrate/migrate.go b/pkg/migrate/migrate.go
index 921f821cb..cfa82b7d7 100644
--- a/pkg/migrate/migrate.go
+++ b/pkg/migrate/migrate.go
@@ -67,7 +67,7 @@ func Run(opts Options) (*Result, error) {
return nil, err
}
- if _, err := os.Stat(openclawHome); os.IsNotExist(err) {
+ if _, err = os.Stat(openclawHome); os.IsNotExist(err) {
return nil, fmt.Errorf("OpenClaw installation not found at %s", openclawHome)
}
@@ -161,7 +161,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
fmt.Printf(" ✓ Converted config: %s\n", action.Destination)
}
case ActionCreateDir:
- if err := os.MkdirAll(action.Destination, 0755); err != nil {
+ if err := os.MkdirAll(action.Destination, 0o755); err != nil {
result.Errors = append(result.Errors, err)
} else {
result.DirsCreated++
@@ -174,9 +174,13 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
continue
}
result.BackupsCreated++
- fmt.Printf(" ✓ Backed up %s -> %s.bak\n", filepath.Base(action.Destination), filepath.Base(action.Destination))
+ fmt.Printf(
+ " ✓ Backed up %s -> %s.bak\n",
+ filepath.Base(action.Destination),
+ filepath.Base(action.Destination),
+ )
- if err := os.MkdirAll(filepath.Dir(action.Destination), 0755); err != nil {
+ if err := os.MkdirAll(filepath.Dir(action.Destination), 0o755); err != nil {
result.Errors = append(result.Errors, err)
continue
}
@@ -188,7 +192,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
fmt.Printf(" ✓ Copied %s\n", relPath(action.Source, openclawHome))
}
case ActionCopy:
- if err := os.MkdirAll(filepath.Dir(action.Destination), 0755); err != nil {
+ if err := os.MkdirAll(filepath.Dir(action.Destination), 0o755); err != nil {
result.Errors = append(result.Errors, err)
continue
}
@@ -226,7 +230,7 @@ func executeConfigMigration(srcConfigPath, dstConfigPath, picoClawHome string) e
incoming = MergeConfig(existing, incoming)
}
- if err := os.MkdirAll(filepath.Dir(dstConfigPath), 0755); err != nil {
+ if err := os.MkdirAll(filepath.Dir(dstConfigPath), 0o755); err != nil {
return err
}
return config.SaveConfig(dstConfigPath, incoming)
diff --git a/pkg/migrate/migrate_test.go b/pkg/migrate/migrate_test.go
index 759fc9024..b6b3d70aa 100644
--- a/pkg/migrate/migrate_test.go
+++ b/pkg/migrate/migrate_test.go
@@ -40,43 +40,43 @@ func TestCamelToSnake(t *testing.T) {
}
func TestConvertKeysToSnake(t *testing.T) {
- input := map[string]interface{}{
+ input := map[string]any{
"apiKey": "test-key",
"apiBase": "https://example.com",
- "nested": map[string]interface{}{
+ "nested": map[string]any{
"maxTokens": float64(8192),
- "allowFrom": []interface{}{"user1", "user2"},
- "deeperLevel": map[string]interface{}{
+ "allowFrom": []any{"user1", "user2"},
+ "deeperLevel": map[string]any{
"clientId": "abc",
},
},
}
result := convertKeysToSnake(input)
- m, ok := result.(map[string]interface{})
+ m, ok := result.(map[string]any)
if !ok {
t.Fatal("expected map[string]interface{}")
}
- if _, ok := m["api_key"]; !ok {
+ if _, ok = m["api_key"]; !ok {
t.Error("expected key 'api_key' after conversion")
}
- if _, ok := m["api_base"]; !ok {
+ if _, ok = m["api_base"]; !ok {
t.Error("expected key 'api_base' after conversion")
}
- nested, ok := m["nested"].(map[string]interface{})
+ nested, ok := m["nested"].(map[string]any)
if !ok {
t.Fatal("expected nested map")
}
- if _, ok := nested["max_tokens"]; !ok {
+ if _, ok = nested["max_tokens"]; !ok {
t.Error("expected key 'max_tokens' in nested map")
}
- if _, ok := nested["allow_from"]; !ok {
+ if _, ok = nested["allow_from"]; !ok {
t.Error("expected key 'allow_from' in nested map")
}
- deeper, ok := nested["deeper_level"].(map[string]interface{})
+ deeper, ok := nested["deeper_level"].(map[string]any)
if !ok {
t.Fatal("expected deeper_level map")
}
@@ -89,15 +89,15 @@ func TestLoadOpenClawConfig(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "openclaw.json")
- openclawConfig := map[string]interface{}{
- "providers": map[string]interface{}{
- "anthropic": map[string]interface{}{
+ openclawConfig := map[string]any{
+ "providers": map[string]any{
+ "anthropic": map[string]any{
"apiKey": "sk-ant-test123",
"apiBase": "https://api.anthropic.com",
},
},
- "agents": map[string]interface{}{
- "defaults": map[string]interface{}{
+ "agents": map[string]any{
+ "defaults": map[string]any{
"maxTokens": float64(4096),
"model": "claude-3-opus",
},
@@ -108,7 +108,7 @@ func TestLoadOpenClawConfig(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(configPath, data, 0644); err != nil {
+ if err = os.WriteFile(configPath, data, 0o644); err != nil {
t.Fatal(err)
}
@@ -117,11 +117,11 @@ func TestLoadOpenClawConfig(t *testing.T) {
t.Fatalf("LoadOpenClawConfig: %v", err)
}
- providers, ok := result["providers"].(map[string]interface{})
+ providers, ok := result["providers"].(map[string]any)
if !ok {
t.Fatal("expected providers map")
}
- anthropic, ok := providers["anthropic"].(map[string]interface{})
+ anthropic, ok := providers["anthropic"].(map[string]any)
if !ok {
t.Fatal("expected anthropic map")
}
@@ -129,11 +129,11 @@ func TestLoadOpenClawConfig(t *testing.T) {
t.Errorf("api_key = %v, want sk-ant-test123", anthropic["api_key"])
}
- agents, ok := result["agents"].(map[string]interface{})
+ agents, ok := result["agents"].(map[string]any)
if !ok {
t.Fatal("expected agents map")
}
- defaults, ok := agents["defaults"].(map[string]interface{})
+ defaults, ok := agents["defaults"].(map[string]any)
if !ok {
t.Fatal("expected defaults map")
}
@@ -144,16 +144,16 @@ func TestLoadOpenClawConfig(t *testing.T) {
func TestConvertConfig(t *testing.T) {
t.Run("providers mapping", func(t *testing.T) {
- data := map[string]interface{}{
- "providers": map[string]interface{}{
- "anthropic": map[string]interface{}{
+ data := map[string]any{
+ "providers": map[string]any{
+ "anthropic": map[string]any{
"api_key": "sk-ant-test",
"api_base": "https://api.anthropic.com",
},
- "openrouter": map[string]interface{}{
+ "openrouter": map[string]any{
"api_key": "sk-or-test",
},
- "groq": map[string]interface{}{
+ "groq": map[string]any{
"api_key": "gsk-test",
},
},
@@ -178,9 +178,9 @@ func TestConvertConfig(t *testing.T) {
})
t.Run("unsupported provider warning", func(t *testing.T) {
- data := map[string]interface{}{
- "providers": map[string]interface{}{
- "unknown_provider": map[string]interface{}{
+ data := map[string]any{
+ "providers": map[string]any{
+ "unknown_provider": map[string]any{
"api_key": "sk-test",
},
},
@@ -199,14 +199,14 @@ func TestConvertConfig(t *testing.T) {
})
t.Run("channels mapping", func(t *testing.T) {
- data := map[string]interface{}{
- "channels": map[string]interface{}{
- "telegram": map[string]interface{}{
+ data := map[string]any{
+ "channels": map[string]any{
+ "telegram": map[string]any{
"enabled": true,
"token": "tg-token-123",
- "allow_from": []interface{}{"user1"},
+ "allow_from": []any{"user1"},
},
- "discord": map[string]interface{}{
+ "discord": map[string]any{
"enabled": true,
"token": "disc-token-456",
},
@@ -232,9 +232,9 @@ func TestConvertConfig(t *testing.T) {
})
t.Run("unsupported channel warning", func(t *testing.T) {
- data := map[string]interface{}{
- "channels": map[string]interface{}{
- "email": map[string]interface{}{
+ data := map[string]any{
+ "channels": map[string]any{
+ "email": map[string]any{
"enabled": true,
},
},
@@ -253,9 +253,9 @@ func TestConvertConfig(t *testing.T) {
})
t.Run("agent defaults", func(t *testing.T) {
- data := map[string]interface{}{
- "agents": map[string]interface{}{
- "defaults": map[string]interface{}{
+ data := map[string]any{
+ "agents": map[string]any{
+ "defaults": map[string]any{
"model": "claude-3-opus",
"max_tokens": float64(4096),
"temperature": 0.5,
@@ -287,7 +287,7 @@ func TestConvertConfig(t *testing.T) {
})
t.Run("empty config", func(t *testing.T) {
- data := map[string]interface{}{}
+ data := map[string]any{}
cfg, warnings, err := ConvertConfig(data)
if err != nil {
@@ -389,9 +389,9 @@ func TestPlanWorkspaceMigration(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
- os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644)
- os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0644)
- os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0644)
+ os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644)
+ os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0o644)
+ os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0o644)
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
if err != nil {
@@ -420,8 +420,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
- os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644)
- os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0644)
+ os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644)
+ os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0o644)
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
if err != nil {
@@ -443,8 +443,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
- os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644)
- os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0644)
+ os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0o644)
+ os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0o644)
actions, err := PlanWorkspaceMigration(srcDir, dstDir, true)
if err != nil {
@@ -463,8 +463,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
dstDir := t.TempDir()
memDir := filepath.Join(srcDir, "memory")
- os.MkdirAll(memDir, 0755)
- os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0644)
+ os.MkdirAll(memDir, 0o755)
+ os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0o644)
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
if err != nil {
@@ -494,8 +494,8 @@ func TestPlanWorkspaceMigration(t *testing.T) {
dstDir := t.TempDir()
skillDir := filepath.Join(srcDir, "skills", "weather")
- os.MkdirAll(skillDir, 0755)
- os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0644)
+ os.MkdirAll(skillDir, 0o755)
+ os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0o644)
actions, err := PlanWorkspaceMigration(srcDir, dstDir, false)
if err != nil {
@@ -518,7 +518,7 @@ func TestFindOpenClawConfig(t *testing.T) {
t.Run("finds openclaw.json", func(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "openclaw.json")
- os.WriteFile(configPath, []byte("{}"), 0644)
+ os.WriteFile(configPath, []byte("{}"), 0o644)
found, err := findOpenClawConfig(tmpDir)
if err != nil {
@@ -532,7 +532,7 @@ func TestFindOpenClawConfig(t *testing.T) {
t.Run("falls back to config.json", func(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.json")
- os.WriteFile(configPath, []byte("{}"), 0644)
+ os.WriteFile(configPath, []byte("{}"), 0o644)
found, err := findOpenClawConfig(tmpDir)
if err != nil {
@@ -546,8 +546,8 @@ func TestFindOpenClawConfig(t *testing.T) {
t.Run("prefers openclaw.json over config.json", func(t *testing.T) {
tmpDir := t.TempDir()
openclawPath := filepath.Join(tmpDir, "openclaw.json")
- os.WriteFile(openclawPath, []byte("{}"), 0644)
- os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0644)
+ os.WriteFile(openclawPath, []byte("{}"), 0o644)
+ os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0o644)
found, err := findOpenClawConfig(tmpDir)
if err != nil {
@@ -593,19 +593,19 @@ func TestRunDryRun(t *testing.T) {
picoClawHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace")
- os.MkdirAll(wsDir, 0755)
- os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
- os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0644)
+ os.MkdirAll(wsDir, 0o755)
+ os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
+ os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0o644)
- configData := map[string]interface{}{
- "providers": map[string]interface{}{
- "anthropic": map[string]interface{}{
+ configData := map[string]any{
+ "providers": map[string]any{
+ "anthropic": map[string]any{
"apiKey": "test-key",
},
},
}
data, _ := json.Marshal(configData)
- os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
+ os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
opts := Options{
DryRun: true,
@@ -634,33 +634,33 @@ func TestRunFullMigration(t *testing.T) {
picoClawHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace")
- os.MkdirAll(wsDir, 0755)
- os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0644)
- os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644)
- os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0644)
+ os.MkdirAll(wsDir, 0o755)
+ os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0o644)
+ os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0o644)
+ os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0o644)
memDir := filepath.Join(wsDir, "memory")
- os.MkdirAll(memDir, 0755)
- os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0644)
+ os.MkdirAll(memDir, 0o755)
+ os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0o644)
- configData := map[string]interface{}{
- "providers": map[string]interface{}{
- "anthropic": map[string]interface{}{
+ configData := map[string]any{
+ "providers": map[string]any{
+ "anthropic": map[string]any{
"apiKey": "sk-ant-migrate-test",
},
- "openrouter": map[string]interface{}{
+ "openrouter": map[string]any{
"apiKey": "sk-or-migrate-test",
},
},
- "channels": map[string]interface{}{
- "telegram": map[string]interface{}{
+ "channels": map[string]any{
+ "telegram": map[string]any{
"enabled": true,
"token": "tg-migrate-test",
},
},
}
data, _ := json.Marshal(configData)
- os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
+ os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
opts := Options{
Force: true,
@@ -754,7 +754,7 @@ func TestRunMutuallyExclusiveFlags(t *testing.T) {
func TestBackupFile(t *testing.T) {
tmpDir := t.TempDir()
filePath := filepath.Join(tmpDir, "test.md")
- os.WriteFile(filePath, []byte("original content"), 0644)
+ os.WriteFile(filePath, []byte("original content"), 0o644)
if err := backupFile(filePath); err != nil {
t.Fatalf("backupFile: %v", err)
@@ -775,7 +775,7 @@ func TestCopyFile(t *testing.T) {
srcPath := filepath.Join(tmpDir, "src.md")
dstPath := filepath.Join(tmpDir, "dst.md")
- os.WriteFile(srcPath, []byte("file content"), 0644)
+ os.WriteFile(srcPath, []byte("file content"), 0o644)
if err := copyFile(srcPath, dstPath); err != nil {
t.Fatalf("copyFile: %v", err)
@@ -795,18 +795,18 @@ func TestRunConfigOnly(t *testing.T) {
picoClawHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace")
- os.MkdirAll(wsDir, 0755)
- os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
+ os.MkdirAll(wsDir, 0o755)
+ os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
- configData := map[string]interface{}{
- "providers": map[string]interface{}{
- "anthropic": map[string]interface{}{
+ configData := map[string]any{
+ "providers": map[string]any{
+ "anthropic": map[string]any{
"apiKey": "sk-config-only",
},
},
}
data, _ := json.Marshal(configData)
- os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
+ os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
opts := Options{
Force: true,
@@ -835,18 +835,18 @@ func TestRunWorkspaceOnly(t *testing.T) {
picoClawHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace")
- os.MkdirAll(wsDir, 0755)
- os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644)
+ os.MkdirAll(wsDir, 0o755)
+ os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0o644)
- configData := map[string]interface{}{
- "providers": map[string]interface{}{
- "anthropic": map[string]interface{}{
+ configData := map[string]any{
+ "providers": map[string]any{
+ "anthropic": map[string]any{
"apiKey": "sk-ws-only",
},
},
}
data, _ := json.Marshal(configData)
- os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
+ os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0o644)
opts := Options{
Force: true,
diff --git a/pkg/providers/anthropic/provider.go b/pkg/providers/anthropic/provider.go
index a27a25a2d..35f6b8f62 100644
--- a/pkg/providers/anthropic/provider.go
+++ b/pkg/providers/anthropic/provider.go
@@ -9,16 +9,19 @@ import (
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
+
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
)
-type ToolCall = protocoltypes.ToolCall
-type FunctionCall = protocoltypes.FunctionCall
-type LLMResponse = protocoltypes.LLMResponse
-type UsageInfo = protocoltypes.UsageInfo
-type Message = protocoltypes.Message
-type ToolDefinition = protocoltypes.ToolDefinition
-type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
+type (
+ ToolCall = protocoltypes.ToolCall
+ FunctionCall = protocoltypes.FunctionCall
+ LLMResponse = protocoltypes.LLMResponse
+ UsageInfo = protocoltypes.UsageInfo
+ Message = protocoltypes.Message
+ ToolDefinition = protocoltypes.ToolDefinition
+ ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
+)
const defaultBaseURL = "https://api.anthropic.com"
@@ -61,7 +64,13 @@ func NewProviderWithTokenSourceAndBaseURL(token string, tokenSource func() (stri
return p
}
-func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *Provider) Chat(
+ ctx context.Context,
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+) (*LLMResponse, error) {
var opts []option.RequestOption
if p.tokenSource != nil {
tok, err := p.tokenSource()
@@ -92,7 +101,12 @@ func (p *Provider) BaseURL() string {
return p.baseURL
}
-func buildParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (anthropic.MessageNewParams, error) {
+func buildParams(
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+) (anthropic.MessageNewParams, error) {
var system []anthropic.TextBlockParam
var anthropicMessages []anthropic.MessageParam
@@ -170,7 +184,7 @@ func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
if desc := t.Function.Description; desc != "" {
tool.Description = anthropic.String(desc)
}
- if req, ok := t.Function.Parameters["required"].([]interface{}); ok {
+ if req, ok := t.Function.Parameters["required"].([]any); ok {
required := make([]string, 0, len(req))
for _, r := range req {
if s, ok := r.(string); ok {
@@ -195,10 +209,10 @@ func parseResponse(resp *anthropic.Message) *LLMResponse {
content += tb.Text
case "tool_use":
tu := block.AsToolUse()
- var args map[string]interface{}
+ var args map[string]any
if err := json.Unmarshal(tu.Input, &args); err != nil {
log.Printf("anthropic: failed to decode tool call input for %q: %v", tu.Name, err)
- args = map[string]interface{}{"raw": string(tu.Input)}
+ args = map[string]any{"raw": string(tu.Input)}
}
toolCalls = append(toolCalls, ToolCall{
ID: tu.ID,
diff --git a/pkg/providers/anthropic/provider_test.go b/pkg/providers/anthropic/provider_test.go
index 08ac9c829..3d21c1d0b 100644
--- a/pkg/providers/anthropic/provider_test.go
+++ b/pkg/providers/anthropic/provider_test.go
@@ -15,7 +15,7 @@ func TestBuildParams_BasicMessage(t *testing.T) {
messages := []Message{
{Role: "user", Content: "Hello"},
}
- params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]interface{}{
+ params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]any{
"max_tokens": 1024,
})
if err != nil {
@@ -37,7 +37,7 @@ func TestBuildParams_SystemMessage(t *testing.T) {
{Role: "system", Content: "You are helpful"},
{Role: "user", Content: "Hi"},
}
- params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]interface{}{})
+ params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]any{})
if err != nil {
t.Fatalf("buildParams() error: %v", err)
}
@@ -62,13 +62,13 @@ func TestBuildParams_ToolCallMessage(t *testing.T) {
{
ID: "call_1",
Name: "get_weather",
- Arguments: map[string]interface{}{"city": "SF"},
+ Arguments: map[string]any{"city": "SF"},
},
},
},
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
}
- params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]interface{}{})
+ params, err := buildParams(messages, nil, "claude-sonnet-4.6", map[string]any{})
if err != nil {
t.Fatalf("buildParams() error: %v", err)
}
@@ -84,17 +84,17 @@ func TestBuildParams_WithTools(t *testing.T) {
Function: ToolFunctionDefinition{
Name: "get_weather",
Description: "Get weather for a city",
- Parameters: map[string]interface{}{
+ Parameters: map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "city": map[string]interface{}{"type": "string"},
+ "properties": map[string]any{
+ "city": map[string]any{"type": "string"},
},
- "required": []interface{}{"city"},
+ "required": []any{"city"},
},
},
},
}
- params, err := buildParams([]Message{{Role: "user", Content: "Hi"}}, tools, "claude-sonnet-4.6", map[string]interface{}{})
+ params, err := buildParams([]Message{{Role: "user", Content: "Hi"}}, tools, "claude-sonnet-4.6", map[string]any{})
if err != nil {
t.Fatalf("buildParams() error: %v", err)
}
@@ -154,19 +154,19 @@ func TestProvider_ChatRoundTrip(t *testing.T) {
return
}
- var reqBody map[string]interface{}
+ var reqBody map[string]any
json.NewDecoder(r.Body).Decode(&reqBody)
- resp := map[string]interface{}{
+ resp := map[string]any{
"id": "msg_test",
"type": "message",
"role": "assistant",
"model": reqBody["model"],
"stop_reason": "end_turn",
- "content": []map[string]interface{}{
+ "content": []map[string]any{
{"type": "text", "text": "Hello! How can I help you?"},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"input_tokens": 15,
"output_tokens": 8,
},
@@ -178,7 +178,7 @@ func TestProvider_ChatRoundTrip(t *testing.T) {
provider := NewProviderWithClient(createAnthropicTestClient(server.URL, "test-token"))
messages := []Message{{Role: "user", Content: "Hello"}}
- resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]interface{}{"max_tokens": 1024})
+ resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 1024})
if err != nil {
t.Fatalf("Chat() error: %v", err)
}
@@ -221,19 +221,19 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) {
return
}
- var reqBody map[string]interface{}
+ var reqBody map[string]any
json.NewDecoder(r.Body).Decode(&reqBody)
- resp := map[string]interface{}{
+ resp := map[string]any{
"id": "msg_test",
"type": "message",
"role": "assistant",
"model": reqBody["model"],
"stop_reason": "end_turn",
- "content": []map[string]interface{}{
+ "content": []map[string]any{
{"type": "text", "text": "ok"},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"input_tokens": 1,
"output_tokens": 1,
},
@@ -247,7 +247,13 @@ func TestProvider_ChatUsesTokenSource(t *testing.T) {
return "refreshed-token", nil
}, server.URL)
- _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hello"}}, nil, "claude-sonnet-4.6", map[string]interface{}{})
+ _, err := p.Chat(
+ t.Context(),
+ []Message{{Role: "user", Content: "hello"}},
+ nil,
+ "claude-sonnet-4.6",
+ map[string]any{},
+ )
if err != nil {
t.Fatalf("Chat() error: %v", err)
}
diff --git a/pkg/providers/antigravity_provider.go b/pkg/providers/antigravity_provider.go
index 6c6bf7830..cff67c88c 100644
--- a/pkg/providers/antigravity_provider.go
+++ b/pkg/providers/antigravity_provider.go
@@ -45,7 +45,13 @@ func NewAntigravityProvider() *AntigravityProvider {
// Chat implements LLMProvider.Chat using the Cloud Code Assist v1internal API.
// The v1internal endpoint wraps the standard Gemini request in an envelope with
// project, model, request, requestType, userAgent, and requestId fields.
-func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *AntigravityProvider) Chat(
+ ctx context.Context,
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+) (*LLMResponse, error) {
accessToken, projectID, err := p.tokenSource()
if err != nil {
return nil, fmt.Errorf("antigravity auth: %w", err)
@@ -58,7 +64,7 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool
model = strings.TrimPrefix(model, "google-antigravity/")
model = strings.TrimPrefix(model, "antigravity/")
- logger.DebugCF("provider.antigravity", "Starting chat", map[string]interface{}{
+ logger.DebugCF("provider.antigravity", "Starting chat", map[string]any{
"model": model,
"project": projectID,
"requestId": fmt.Sprintf("agent-%d-%s", time.Now().UnixMilli(), randomString(9)),
@@ -68,7 +74,7 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool
innerRequest := p.buildRequest(messages, tools, model, options)
// Wrap in v1internal envelope (matches pi-ai SDK format)
- envelope := map[string]interface{}{
+ envelope := map[string]any{
"project": projectID,
"model": model,
"request": innerRequest,
@@ -115,7 +121,7 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool
}
if resp.StatusCode != http.StatusOK {
- logger.ErrorCF("provider.antigravity", "API call failed", map[string]interface{}{
+ logger.ErrorCF("provider.antigravity", "API call failed", map[string]any{
"status_code": resp.StatusCode,
"response": string(respBody),
"model": model,
@@ -133,7 +139,9 @@ func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tool
// Check for empty response (some models might return valid success but empty text)
if llmResp.Content == "" && len(llmResp.ToolCalls) == 0 {
- return nil, fmt.Errorf("antigravity: model returned an empty response (this model might be invalid or restricted)")
+ return nil, fmt.Errorf(
+ "antigravity: model returned an empty response (this model might be invalid or restricted)",
+ )
}
return llmResp, nil
@@ -167,13 +175,13 @@ type antigravityPart struct {
}
type antigravityFunctionCall struct {
- Name string `json:"name"`
- Args map[string]interface{} `json:"args"`
+ Name string `json:"name"`
+ Args map[string]any `json:"args"`
}
type antigravityFunctionResponse struct {
- Name string `json:"name"`
- Response map[string]interface{} `json:"response"`
+ Name string `json:"name"`
+ Response map[string]any `json:"response"`
}
type antigravityTool struct {
@@ -181,9 +189,9 @@ type antigravityTool struct {
}
type antigravityFuncDecl struct {
- Name string `json:"name"`
- Description string `json:"description,omitempty"`
- Parameters interface{} `json:"parameters,omitempty"`
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ Parameters any `json:"parameters,omitempty"`
}
type antigravitySystemPrompt struct {
@@ -195,7 +203,12 @@ type antigravityGenConfig struct {
Temperature float64 `json:"temperature,omitempty"`
}
-func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) antigravityRequest {
+func (p *AntigravityProvider) buildRequest(
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+) antigravityRequest {
req := antigravityRequest{}
toolCallNames := make(map[string]string)
@@ -215,7 +228,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
Parts: []antigravityPart{{
FunctionResponse: &antigravityFunctionResponse{
Name: toolName,
- Response: map[string]interface{}{
+ Response: map[string]any{
"result": msg.Content,
},
},
@@ -237,9 +250,13 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
for _, tc := range msg.ToolCalls {
toolName, toolArgs, thoughtSignature := normalizeStoredToolCall(tc)
if toolName == "" {
- logger.WarnCF("provider.antigravity", "Skipping tool call with empty name in history", map[string]interface{}{
- "tool_call_id": tc.ID,
- })
+ logger.WarnCF(
+ "provider.antigravity",
+ "Skipping tool call with empty name in history",
+ map[string]any{
+ "tool_call_id": tc.ID,
+ },
+ )
continue
}
if tc.ID != "" {
@@ -264,7 +281,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
Parts: []antigravityPart{{
FunctionResponse: &antigravityFunctionResponse{
Name: toolName,
- Response: map[string]interface{}{
+ Response: map[string]any{
"result": msg.Content,
},
},
@@ -311,7 +328,7 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
return req
}
-func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}, string) {
+func normalizeStoredToolCall(tc ToolCall) (string, map[string]any, string) {
name := tc.Name
args := tc.Arguments
thoughtSignature := ""
@@ -324,11 +341,11 @@ func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}, strin
}
if args == nil {
- args = map[string]interface{}{}
+ args = map[string]any{}
}
if len(args) == 0 && tc.Function != nil && tc.Function.Arguments != "" {
- var parsed map[string]interface{}
+ var parsed map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &parsed); err == nil && parsed != nil {
args = parsed
}
@@ -483,9 +500,12 @@ func (p *AntigravityProvider) parseSSEResponse(body string) (*LLMResponse, error
Name: part.FunctionCall.Name,
Arguments: part.FunctionCall.Args,
Function: &FunctionCall{
- Name: part.FunctionCall.Name,
- Arguments: string(argumentsJSON),
- ThoughtSignature: extractPartThoughtSignature(part.ThoughtSignature, part.ThoughtSignatureSnake),
+ Name: part.FunctionCall.Name,
+ Arguments: string(argumentsJSON),
+ ThoughtSignature: extractPartThoughtSignature(
+ part.ThoughtSignature,
+ part.ThoughtSignatureSnake,
+ ),
},
})
}
@@ -556,24 +576,24 @@ var geminiUnsupportedKeywords = map[string]bool{
"maxProperties": true,
}
-func sanitizeSchemaForGemini(schema map[string]interface{}) map[string]interface{} {
+func sanitizeSchemaForGemini(schema map[string]any) map[string]any {
if schema == nil {
return nil
}
- result := make(map[string]interface{})
+ result := make(map[string]any)
for k, v := range schema {
if geminiUnsupportedKeywords[k] {
continue
}
// Recursively sanitize nested objects
switch val := v.(type) {
- case map[string]interface{}:
+ case map[string]any:
result[k] = sanitizeSchemaForGemini(val)
- case []interface{}:
- sanitized := make([]interface{}, len(val))
+ case []any:
+ sanitized := make([]any, len(val))
for i, item := range val {
- if m, ok := item.(map[string]interface{}); ok {
+ if m, ok := item.(map[string]any); ok {
sanitized[i] = sanitizeSchemaForGemini(m)
} else {
sanitized[i] = item
@@ -604,7 +624,9 @@ func createAntigravityTokenSource() func() (string, string, error) {
return "", "", fmt.Errorf("loading auth credentials: %w", err)
}
if cred == nil {
- return "", "", fmt.Errorf("no credentials for google-antigravity. Run: picoclaw auth login --provider google-antigravity")
+ return "", "", fmt.Errorf(
+ "no credentials for google-antigravity. Run: picoclaw auth login --provider google-antigravity",
+ )
}
// Refresh if needed
@@ -625,7 +647,9 @@ func createAntigravityTokenSource() func() (string, string, error) {
}
if cred.IsExpired() {
- return "", "", fmt.Errorf("antigravity credentials expired. Run: picoclaw auth login --provider google-antigravity")
+ return "", "", fmt.Errorf(
+ "antigravity credentials expired. Run: picoclaw auth login --provider google-antigravity",
+ )
}
projectID := cred.ProjectID
@@ -633,7 +657,7 @@ func createAntigravityTokenSource() func() (string, string, error) {
// Try to fetch project ID from API
fetchedID, err := FetchAntigravityProjectID(cred.AccessToken)
if err != nil {
- logger.WarnCF("provider.antigravity", "Could not fetch project ID, using fallback", map[string]interface{}{
+ logger.WarnCF("provider.antigravity", "Could not fetch project ID, using fallback", map[string]any{
"error": err.Error(),
})
projectID = "rising-fact-p41fc" // Default fallback (same as OpenCode)
@@ -650,8 +674,8 @@ func createAntigravityTokenSource() func() (string, string, error) {
// FetchAntigravityProjectID retrieves the Google Cloud project ID from the loadCodeAssist endpoint.
func FetchAntigravityProjectID(accessToken string) (string, error) {
- reqBody, _ := json.Marshal(map[string]interface{}{
- "metadata": map[string]interface{}{
+ reqBody, _ := json.Marshal(map[string]any{
+ "metadata": map[string]any{
"ideType": "IDE_UNSPECIFIED",
"platform": "PLATFORM_UNSPECIFIED",
"pluginType": "GEMINI",
@@ -695,7 +719,7 @@ func FetchAntigravityProjectID(accessToken string) (string, error) {
// FetchAntigravityModels fetches available models from the Cloud Code Assist API.
func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelInfo, error) {
- reqBody, _ := json.Marshal(map[string]interface{}{
+ reqBody, _ := json.Marshal(map[string]any{
"project": projectID,
})
@@ -717,16 +741,20 @@ func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelIn
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("fetchAvailableModels failed (HTTP %d): %s", resp.StatusCode, truncateString(string(body), 200))
+ return nil, fmt.Errorf(
+ "fetchAvailableModels failed (HTTP %d): %s",
+ resp.StatusCode,
+ truncateString(string(body), 200),
+ )
}
var result struct {
Models map[string]struct {
DisplayName string `json:"displayName"`
QuotaInfo struct {
- RemainingFraction interface{} `json:"remainingFraction"`
- ResetTime string `json:"resetTime"`
- IsExhausted bool `json:"isExhausted"`
+ RemainingFraction any `json:"remainingFraction"`
+ ResetTime string `json:"resetTime"`
+ IsExhausted bool `json:"isExhausted"`
} `json:"quotaInfo"`
} `json:"models"`
}
@@ -797,10 +825,10 @@ func randomString(n int) string {
func (p *AntigravityProvider) parseAntigravityError(statusCode int, body []byte) error {
var errResp struct {
Error struct {
- Code int `json:"code"`
- Message string `json:"message"`
- Status string `json:"status"`
- Details []map[string]interface{} `json:"details"`
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Status string `json:"status"`
+ Details []map[string]any `json:"details"`
} `json:"error"`
}
@@ -813,7 +841,7 @@ func (p *AntigravityProvider) parseAntigravityError(statusCode int, body []byte)
// Try to extract quota reset info
for _, detail := range errResp.Error.Details {
if typeVal, ok := detail["@type"].(string); ok && strings.HasSuffix(typeVal, "ErrorInfo") {
- if metadata, ok := detail["metadata"].(map[string]interface{}); ok {
+ if metadata, ok := detail["metadata"].(map[string]any); ok {
if delay, ok := metadata["quotaResetDelay"].(string); ok {
return fmt.Errorf("antigravity rate limit exceeded: %s (reset in %s)", msg, delay)
}
diff --git a/pkg/providers/claude_cli_provider.go b/pkg/providers/claude_cli_provider.go
index 58ba3647d..74ec33b98 100644
--- a/pkg/providers/claude_cli_provider.go
+++ b/pkg/providers/claude_cli_provider.go
@@ -24,7 +24,9 @@ func NewClaudeCliProvider(workspace string) *ClaudeCliProvider {
}
// Chat implements LLMProvider.Chat by executing the claude CLI.
-func (p *ClaudeCliProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *ClaudeCliProvider) Chat(
+ ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
+) (*LLMResponse, error) {
systemPrompt := p.buildSystemPrompt(messages, tools)
prompt := p.messagesToPrompt(messages)
@@ -111,7 +113,9 @@ func (p *ClaudeCliProvider) buildToolsPrompt(tools []ToolDefinition) string {
sb.WriteString("## Available Tools\n\n")
sb.WriteString("When you need to use a tool, respond with ONLY a JSON object:\n\n")
sb.WriteString("```json\n")
- sb.WriteString(`{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`)
+ sb.WriteString(
+ `{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`,
+ )
sb.WriteString("\n```\n\n")
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
sb.WriteString("### Tool Definitions:\n\n")
diff --git a/pkg/providers/claude_cli_provider_integration_test.go b/pkg/providers/claude_cli_provider_integration_test.go
index 9d1131ac4..f6e0d787a 100644
--- a/pkg/providers/claude_cli_provider_integration_test.go
+++ b/pkg/providers/claude_cli_provider_integration_test.go
@@ -28,7 +28,6 @@ func TestIntegration_RealClaudeCLI(t *testing.T) {
resp, err := p.Chat(ctx, []Message{
{Role: "user", Content: "Respond with only the word 'pong'. Nothing else."},
}, nil, "", nil)
-
if err != nil {
t.Fatalf("Chat() with real CLI error = %v", err)
}
@@ -75,7 +74,6 @@ func TestIntegration_RealClaudeCLI_WithSystemPrompt(t *testing.T) {
{Role: "system", Content: "You are a calculator. Only respond with numbers. No text."},
{Role: "user", Content: "What is 2+2?"},
}, nil, "", nil)
-
if err != nil {
t.Fatalf("Chat() error = %v", err)
}
diff --git a/pkg/providers/claude_cli_provider_test.go b/pkg/providers/claude_cli_provider_test.go
index 945f5bd4f..3a3cafaca 100644
--- a/pkg/providers/claude_cli_provider_test.go
+++ b/pkg/providers/claude_cli_provider_test.go
@@ -30,12 +30,12 @@ func createMockCLI(t *testing.T, stdout, stderr string, exitCode int) string {
dir := t.TempDir()
if stdout != "" {
- if err := os.WriteFile(filepath.Join(dir, "stdout.txt"), []byte(stdout), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, "stdout.txt"), []byte(stdout), 0o644); err != nil {
t.Fatal(err)
}
}
if stderr != "" {
- if err := os.WriteFile(filepath.Join(dir, "stderr.txt"), []byte(stderr), 0644); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, "stderr.txt"), []byte(stderr), 0o644); err != nil {
t.Fatal(err)
}
}
@@ -51,7 +51,7 @@ func createMockCLI(t *testing.T, stdout, stderr string, exitCode int) string {
sb.WriteString(fmt.Sprintf("exit %d\n", exitCode))
script := filepath.Join(dir, "claude")
- if err := os.WriteFile(script, []byte(sb.String()), 0755); err != nil {
+ if err := os.WriteFile(script, []byte(sb.String()), 0o755); err != nil {
t.Fatal(err)
}
return script
@@ -67,7 +67,7 @@ func createSlowMockCLI(t *testing.T, sleepSeconds int) string {
dir := t.TempDir()
script := filepath.Join(dir, "claude")
content := fmt.Sprintf("#!/bin/sh\nsleep %d\necho '{\"type\":\"result\",\"result\":\"late\"}'\n", sleepSeconds)
- if err := os.WriteFile(script, []byte(content), 0755); err != nil {
+ if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
t.Fatal(err)
}
return script
@@ -88,7 +88,7 @@ cat <<'EOFMOCK'
{"type":"result","result":"ok","session_id":"test"}
EOFMOCK
`, argsFile)
- if err := os.WriteFile(script, []byte(content), 0755); err != nil {
+ if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
t.Fatal(err)
}
return script
@@ -137,7 +137,6 @@ func TestChat_Success(t *testing.T) {
resp, err := p.Chat(context.Background(), []Message{
{Role: "user", Content: "Hello"},
}, nil, "", nil)
-
if err != nil {
t.Fatalf("Chat() error = %v", err)
}
@@ -193,7 +192,6 @@ func TestChat_WithToolCallsInResponse(t *testing.T) {
resp, err := p.Chat(context.Background(), []Message{
{Role: "user", Content: "What's the weather?"},
}, nil, "", nil)
-
if err != nil {
t.Fatalf("Chat() error = %v", err)
}
@@ -403,7 +401,6 @@ func TestChat_EmptyWorkspaceDoesNotSetDir(t *testing.T) {
resp, err := p.Chat(context.Background(), []Message{
{Role: "user", Content: "Hello"},
}, nil, "", nil)
-
if err != nil {
t.Fatalf("Chat() with empty workspace error = %v", err)
}
@@ -622,10 +619,10 @@ func TestBuildSystemPrompt_WithTools(t *testing.T) {
Function: ToolFunctionDefinition{
Name: "get_weather",
Description: "Get weather for a location",
- Parameters: map[string]interface{}{
+ Parameters: map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "location": map[string]interface{}{"type": "string"},
+ "properties": map[string]any{
+ "location": map[string]any{"type": "string"},
},
},
},
diff --git a/pkg/providers/claude_provider.go b/pkg/providers/claude_provider.go
index 3ca54d5a3..60639ca18 100644
--- a/pkg/providers/claude_provider.go
+++ b/pkg/providers/claude_provider.go
@@ -29,7 +29,9 @@ func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string,
}
}
-func NewClaudeProviderWithTokenSourceAndBaseURL(token string, tokenSource func() (string, error), apiBase string) *ClaudeProvider {
+func NewClaudeProviderWithTokenSourceAndBaseURL(
+ token string, tokenSource func() (string, error), apiBase string,
+) *ClaudeProvider {
return &ClaudeProvider{
delegate: anthropicprovider.NewProviderWithTokenSourceAndBaseURL(token, tokenSource, apiBase),
}
@@ -39,7 +41,9 @@ func newClaudeProviderWithDelegate(delegate *anthropicprovider.Provider) *Claude
return &ClaudeProvider{delegate: delegate}
}
-func (p *ClaudeProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *ClaudeProvider) Chat(
+ ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
+) (*LLMResponse, error) {
resp, err := p.delegate.Chat(ctx, messages, tools, model, options)
if err != nil {
return nil, err
diff --git a/pkg/providers/claude_provider_test.go b/pkg/providers/claude_provider_test.go
index b1bcd8b40..98e07bb80 100644
--- a/pkg/providers/claude_provider_test.go
+++ b/pkg/providers/claude_provider_test.go
@@ -8,6 +8,7 @@ import (
"github.com/anthropics/anthropic-sdk-go"
anthropicoption "github.com/anthropics/anthropic-sdk-go/option"
+
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
)
@@ -22,19 +23,19 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) {
return
}
- var reqBody map[string]interface{}
+ var reqBody map[string]any
json.NewDecoder(r.Body).Decode(&reqBody)
- resp := map[string]interface{}{
+ resp := map[string]any{
"id": "msg_test",
"type": "message",
"role": "assistant",
"model": reqBody["model"],
"stop_reason": "end_turn",
- "content": []map[string]interface{}{
+ "content": []map[string]any{
{"type": "text", "text": "Hello! How can I help you?"},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"input_tokens": 15,
"output_tokens": 8,
},
@@ -48,7 +49,7 @@ func TestClaudeProvider_ChatRoundTrip(t *testing.T) {
provider := newClaudeProviderWithDelegate(delegate)
messages := []Message{{Role: "user", Content: "Hello"}}
- resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]interface{}{"max_tokens": 1024})
+ resp, err := provider.Chat(t.Context(), messages, nil, "claude-sonnet-4.6", map[string]any{"max_tokens": 1024})
if err != nil {
t.Fatalf("Chat() error: %v", err)
}
diff --git a/pkg/providers/codex_cli_credentials.go b/pkg/providers/codex_cli_credentials.go
index 7ad39ce8e..40f3ee2a1 100644
--- a/pkg/providers/codex_cli_credentials.go
+++ b/pkg/providers/codex_cli_credentials.go
@@ -31,7 +31,7 @@ func ReadCodexCliCredentials() (accessToken, accountID string, expiresAt time.Ti
}
var auth CodexCliAuth
- if err := json.Unmarshal(data, &auth); err != nil {
+ if err = json.Unmarshal(data, &auth); err != nil {
return "", "", time.Time{}, fmt.Errorf("parsing %s: %w", authPath, err)
}
@@ -59,7 +59,9 @@ func CreateCodexCliTokenSource() func() (string, string, error) {
}
if time.Now().After(expiresAt) {
- return "", "", fmt.Errorf("codex cli credentials expired (auth.json last modified > 1h ago). Run: codex login")
+ return "", "", fmt.Errorf(
+ "codex cli credentials expired (auth.json last modified > 1h ago). Run: codex login",
+ )
}
return token, accountID, nil
diff --git a/pkg/providers/codex_cli_credentials_test.go b/pkg/providers/codex_cli_credentials_test.go
index 3267f2d16..43b21700a 100644
--- a/pkg/providers/codex_cli_credentials_test.go
+++ b/pkg/providers/codex_cli_credentials_test.go
@@ -18,7 +18,7 @@ func TestReadCodexCliCredentials_Valid(t *testing.T) {
"account_id": "org-test123"
}
}`
- if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
+ if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
t.Fatal(err)
}
@@ -58,7 +58,7 @@ func TestReadCodexCliCredentials_EmptyToken(t *testing.T) {
authPath := filepath.Join(tmpDir, "auth.json")
authJSON := `{"tokens": {"access_token": "", "refresh_token": "r", "account_id": "a"}}`
- if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
+ if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
t.Fatal(err)
}
@@ -74,7 +74,7 @@ func TestReadCodexCliCredentials_InvalidJSON(t *testing.T) {
tmpDir := t.TempDir()
authPath := filepath.Join(tmpDir, "auth.json")
- if err := os.WriteFile(authPath, []byte("not json"), 0600); err != nil {
+ if err := os.WriteFile(authPath, []byte("not json"), 0o600); err != nil {
t.Fatal(err)
}
@@ -91,7 +91,7 @@ func TestReadCodexCliCredentials_NoAccountID(t *testing.T) {
authPath := filepath.Join(tmpDir, "auth.json")
authJSON := `{"tokens": {"access_token": "tok123", "refresh_token": "ref456"}}`
- if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
+ if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
t.Fatal(err)
}
@@ -112,12 +112,12 @@ func TestReadCodexCliCredentials_NoAccountID(t *testing.T) {
func TestReadCodexCliCredentials_CodexHomeEnv(t *testing.T) {
tmpDir := t.TempDir()
customDir := filepath.Join(tmpDir, "custom-codex")
- if err := os.MkdirAll(customDir, 0755); err != nil {
+ if err := os.MkdirAll(customDir, 0o755); err != nil {
t.Fatal(err)
}
authJSON := `{"tokens": {"access_token": "custom-token", "refresh_token": "r"}}`
- if err := os.WriteFile(filepath.Join(customDir, "auth.json"), []byte(authJSON), 0600); err != nil {
+ if err := os.WriteFile(filepath.Join(customDir, "auth.json"), []byte(authJSON), 0o600); err != nil {
t.Fatal(err)
}
@@ -137,7 +137,7 @@ func TestCreateCodexCliTokenSource_Valid(t *testing.T) {
authPath := filepath.Join(tmpDir, "auth.json")
authJSON := `{"tokens": {"access_token": "fresh-token", "refresh_token": "r", "account_id": "acc"}}`
- if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
+ if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
t.Fatal(err)
}
@@ -161,7 +161,7 @@ func TestCreateCodexCliTokenSource_Expired(t *testing.T) {
authPath := filepath.Join(tmpDir, "auth.json")
authJSON := `{"tokens": {"access_token": "old-token", "refresh_token": "r"}}`
- if err := os.WriteFile(authPath, []byte(authJSON), 0600); err != nil {
+ if err := os.WriteFile(authPath, []byte(authJSON), 0o600); err != nil {
t.Fatal(err)
}
diff --git a/pkg/providers/codex_cli_provider.go b/pkg/providers/codex_cli_provider.go
index 8886406b4..4c783ece5 100644
--- a/pkg/providers/codex_cli_provider.go
+++ b/pkg/providers/codex_cli_provider.go
@@ -25,7 +25,9 @@ func NewCodexCliProvider(workspace string) *CodexCliProvider {
}
// Chat implements LLMProvider.Chat by executing the codex CLI in non-interactive mode.
-func (p *CodexCliProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *CodexCliProvider) Chat(
+ ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
+) (*LLMResponse, error) {
if p.command == "" {
return nil, fmt.Errorf("codex command not configured")
}
@@ -133,7 +135,9 @@ func (p *CodexCliProvider) buildToolsPrompt(tools []ToolDefinition) string {
sb.WriteString("## Available Tools\n\n")
sb.WriteString("When you need to use a tool, respond with ONLY a JSON object:\n\n")
sb.WriteString("```json\n")
- sb.WriteString(`{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`)
+ sb.WriteString(
+ `{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{...}"}}]}`,
+ )
sb.WriteString("\n```\n\n")
sb.WriteString("CRITICAL: The 'arguments' field MUST be a JSON-encoded STRING.\n\n")
sb.WriteString("### Tool Definitions:\n\n")
diff --git a/pkg/providers/codex_cli_provider_integration_test.go b/pkg/providers/codex_cli_provider_integration_test.go
index 0267c730f..17a8305ad 100644
--- a/pkg/providers/codex_cli_provider_integration_test.go
+++ b/pkg/providers/codex_cli_provider_integration_test.go
@@ -27,7 +27,6 @@ func TestIntegration_RealCodexCLI(t *testing.T) {
resp, err := p.Chat(ctx, []Message{
{Role: "user", Content: "Respond with only the word 'pong'. Nothing else."},
}, nil, "", nil)
-
if err != nil {
t.Fatalf("Chat() with real CLI error = %v", err)
}
@@ -64,7 +63,6 @@ func TestIntegration_RealCodexCLI_WithSystemPrompt(t *testing.T) {
{Role: "system", Content: "You are a calculator. Only respond with numbers. No text."},
{Role: "user", Content: "What is 2+2?"},
}, nil, "", nil)
-
if err != nil {
t.Fatalf("Chat() error = %v", err)
}
diff --git a/pkg/providers/codex_cli_provider_test.go b/pkg/providers/codex_cli_provider_test.go
index 7e4e1bc15..414e0844d 100644
--- a/pkg/providers/codex_cli_provider_test.go
+++ b/pkg/providers/codex_cli_provider_test.go
@@ -292,10 +292,10 @@ func TestBuildPrompt_WithTools(t *testing.T) {
Function: ToolFunctionDefinition{
Name: "get_weather",
Description: "Get current weather",
- Parameters: map[string]interface{}{
+ Parameters: map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "city": map[string]interface{}{"type": "string"},
+ "properties": map[string]any{
+ "city": map[string]any{"type": "string"},
},
},
},
@@ -409,7 +409,7 @@ func createMockCodexCLI(t *testing.T, events []string) string {
sb.WriteString(fmt.Sprintf("echo '%s'\n", event))
}
- if err := os.WriteFile(scriptPath, []byte(sb.String()), 0755); err != nil {
+ if err := os.WriteFile(scriptPath, []byte(sb.String()), 0o755); err != nil {
t.Fatal(err)
}
return scriptPath
@@ -480,7 +480,7 @@ echo "$@" > "` + filepath.Join(tmpDir, "args.txt") + `"
echo '{"type":"item.completed","item":{"id":"1","type":"agent_message","text":"ok"}}'
echo '{"type":"turn.completed"}'`
- if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
+ if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
@@ -522,7 +522,7 @@ func TestCodexCliProvider_MockCLI_ContextCancel(t *testing.T) {
scriptPath := filepath.Join(tmpDir, "codex")
script := "#!/bin/bash\nsleep 60"
- if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
+ if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
diff --git a/pkg/providers/codex_provider.go b/pkg/providers/codex_provider.go
index e3526cfb5..ecc983642 100644
--- a/pkg/providers/codex_provider.go
+++ b/pkg/providers/codex_provider.go
@@ -10,12 +10,15 @@ import (
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
+
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/logger"
)
-const codexDefaultModel = "gpt-5.2"
-const codexDefaultInstructions = "You are Codex, a coding assistant."
+const (
+ codexDefaultModel = "gpt-5.2"
+ codexDefaultInstructions = "You are Codex, a coding assistant."
+)
type CodexProvider struct {
client *openai.Client
@@ -44,22 +47,30 @@ func NewCodexProvider(token, accountID string) *CodexProvider {
}
}
-func NewCodexProviderWithTokenSource(token, accountID string, tokenSource func() (string, string, error)) *CodexProvider {
+func NewCodexProviderWithTokenSource(
+ token, accountID string, tokenSource func() (string, string, error),
+) *CodexProvider {
p := NewCodexProvider(token, accountID)
p.tokenSource = tokenSource
return p
}
-func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *CodexProvider) Chat(
+ ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
+) (*LLMResponse, error) {
var opts []option.RequestOption
accountID := p.accountID
resolvedModel, fallbackReason := resolveCodexModel(model)
if fallbackReason != "" {
- logger.WarnCF("provider.codex", "Requested model is not compatible with Codex backend, using fallback", map[string]interface{}{
- "requested_model": model,
- "resolved_model": resolvedModel,
- "reason": fallbackReason,
- })
+ logger.WarnCF(
+ "provider.codex",
+ "Requested model is not compatible with Codex backend, using fallback",
+ map[string]any{
+ "requested_model": model,
+ "resolved_model": resolvedModel,
+ "reason": fallbackReason,
+ },
+ )
}
if p.tokenSource != nil {
tok, accID, err := p.tokenSource()
@@ -74,10 +85,14 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
if accountID != "" {
opts = append(opts, option.WithHeader("Chatgpt-Account-Id", accountID))
} else {
- logger.WarnCF("provider.codex", "No account id found for Codex request; backend may reject with 400", map[string]interface{}{
- "requested_model": model,
- "resolved_model": resolvedModel,
- })
+ logger.WarnCF(
+ "provider.codex",
+ "No account id found for Codex request; backend may reject with 400",
+ map[string]any{
+ "requested_model": model,
+ "resolved_model": resolvedModel,
+ },
+ )
}
params := buildCodexParams(messages, tools, resolvedModel, options, p.enableWebSearch)
@@ -98,7 +113,7 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
}
err := stream.Err()
if err != nil {
- fields := map[string]interface{}{
+ fields := map[string]any{
"requested_model": model,
"resolved_model": resolvedModel,
"messages_count": len(messages),
@@ -124,7 +139,7 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
return nil, fmt.Errorf("codex API call: %w", err)
}
if resp == nil {
- fields := map[string]interface{}{
+ fields := map[string]any{
"requested_model": model,
"resolved_model": resolvedModel,
"messages_count": len(messages),
@@ -184,7 +199,9 @@ func resolveCodexModel(model string) (string, string) {
return codexDefaultModel, "unsupported model family"
}
-func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, enableWebSearch bool) responses.ResponseNewParams {
+func buildCodexParams(
+ messages []Message, tools []ToolDefinition, model string, options map[string]any, enableWebSearch bool,
+) responses.ResponseNewParams {
var inputItems responses.ResponseInputParam
var instructions string
@@ -197,7 +214,9 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{
CallID: msg.ToolCallID,
- Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{OfString: openai.Opt(msg.Content)},
+ Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{
+ OfString: openai.Opt(msg.Content),
+ },
},
})
} else {
@@ -221,7 +240,7 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
for _, tc := range msg.ToolCalls {
name, args, ok := resolveCodexToolCall(tc)
if !ok {
- logger.WarnCF("provider.codex", "Skipping invalid tool call in history", map[string]interface{}{
+ logger.WarnCF("provider.codex", "Skipping invalid tool call in history", map[string]any{
"call_id": tc.ID,
})
continue
@@ -246,7 +265,9 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{
CallID: msg.ToolCallID,
- Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{OfString: openai.Opt(msg.Content)},
+ Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{
+ OfString: openai.Opt(msg.Content),
+ },
},
})
}
@@ -341,9 +362,9 @@ func parseCodexResponse(resp *responses.Response) *LLMResponse {
}
}
case "function_call":
- var args map[string]interface{}
+ var args map[string]any
if err := json.Unmarshal([]byte(item.Arguments), &args); err != nil {
- args = map[string]interface{}{"raw": item.Arguments}
+ args = map[string]any{"raw": item.Arguments}
}
toolCalls = append(toolCalls, ToolCall{
ID: item.CallID,
diff --git a/pkg/providers/codex_provider_test.go b/pkg/providers/codex_provider_test.go
index 92e276165..4157e53e9 100644
--- a/pkg/providers/codex_provider_test.go
+++ b/pkg/providers/codex_provider_test.go
@@ -16,7 +16,7 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) {
messages := []Message{
{Role: "user", Content: "Hello"},
}
- params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{
"max_tokens": 2048,
"temperature": 0.7,
}, true)
@@ -39,7 +39,7 @@ func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
{Role: "system", Content: "You are helpful"},
{Role: "user", Content: "Hi"},
}
- params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, true)
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{}, true)
if !params.Instructions.Valid() {
t.Fatal("Instructions should be set")
}
@@ -54,12 +54,12 @@ func TestBuildCodexParams_ToolCallConversation(t *testing.T) {
{
Role: "assistant",
ToolCalls: []ToolCall{
- {ID: "call_1", Name: "get_weather", Arguments: map[string]interface{}{"city": "SF"}},
+ {ID: "call_1", Name: "get_weather", Arguments: map[string]any{"city": "SF"}},
},
},
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
}
- params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{}, false)
if params.Input.OfInputItemList == nil {
t.Fatal("Input.OfInputItemList should not be nil")
}
@@ -87,7 +87,7 @@ func TestBuildCodexParams_ToolCallFunctionFallback(t *testing.T) {
{Role: "tool", Content: "ok", ToolCallID: "call_1"},
}
- params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]any{}, false)
if params.Input.OfInputItemList == nil {
t.Fatal("Input.OfInputItemList should not be nil")
}
@@ -114,16 +114,16 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
Function: ToolFunctionDefinition{
Name: "get_weather",
Description: "Get weather",
- Parameters: map[string]interface{}{
+ Parameters: map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "city": map[string]interface{}{"type": "string"},
+ "properties": map[string]any{
+ "city": map[string]any{"type": "string"},
},
},
},
},
}
- params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, false)
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]any{}, false)
if len(params.Tools) != 1 {
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
}
@@ -136,14 +136,14 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
}
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
- params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, false)
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]any{}, false)
if !params.Store.Valid() || params.Store.Or(true) != false {
t.Error("Store should be explicitly set to false")
}
}
func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
- params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]any{}, true)
if len(params.Tools) != 1 {
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
}
@@ -151,7 +151,11 @@ func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
t.Fatal("Tool should include built-in web_search")
}
if params.Tools[0].OfWebSearch.Type != responses.WebSearchToolTypeWebSearch {
- t.Errorf("Web search tool type = %q, want %q", params.Tools[0].OfWebSearch.Type, responses.WebSearchToolTypeWebSearch)
+ t.Errorf(
+ "Web search tool type = %q, want %q",
+ params.Tools[0].OfWebSearch.Type,
+ responses.WebSearchToolTypeWebSearch,
+ )
}
}
@@ -162,7 +166,7 @@ func TestBuildCodexParams_WebSearchFunctionReplacedWithBuiltin(t *testing.T) {
Function: ToolFunctionDefinition{
Name: "web_search",
Description: "local web search",
- Parameters: map[string]interface{}{
+ Parameters: map[string]any{
"type": "object",
},
},
@@ -172,14 +176,14 @@ func TestBuildCodexParams_WebSearchFunctionReplacedWithBuiltin(t *testing.T) {
Function: ToolFunctionDefinition{
Name: "read_file",
Description: "read file",
- Parameters: map[string]interface{}{
+ Parameters: map[string]any{
"type": "object",
},
},
},
}
- params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, true)
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]any{}, true)
if len(params.Tools) != 2 {
t.Fatalf("len(Tools) = %d, want 2", len(params.Tools))
}
@@ -296,7 +300,7 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
return
}
- var reqBody map[string]interface{}
+ var reqBody map[string]any
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
@@ -309,38 +313,38 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
return
}
- toolsAny, ok := reqBody["tools"].([]interface{})
+ toolsAny, ok := reqBody["tools"].([]any)
if !ok || len(toolsAny) != 1 {
http.Error(w, "missing default web search tool", http.StatusBadRequest)
return
}
- toolObj, ok := toolsAny[0].(map[string]interface{})
+ toolObj, ok := toolsAny[0].(map[string]any)
if !ok || toolObj["type"] != "web_search" {
http.Error(w, "expected web_search tool", http.StatusBadRequest)
return
}
- resp := map[string]interface{}{
+ resp := map[string]any{
"id": "resp_test",
"object": "response",
"status": "completed",
- "output": []map[string]interface{}{
+ "output": []map[string]any{
{
"id": "msg_1",
"type": "message",
"role": "assistant",
"status": "completed",
- "content": []map[string]interface{}{
+ "content": []map[string]any{
{"type": "output_text", "text": "Hi from Codex!"},
},
},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"input_tokens": 12,
"output_tokens": 6,
"total_tokens": 18,
- "input_tokens_details": map[string]interface{}{"cached_tokens": 0},
- "output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
+ "input_tokens_details": map[string]any{"cached_tokens": 0},
+ "output_tokens_details": map[string]any{"reasoning_tokens": 0},
},
}
writeCompletedSSE(w, resp)
@@ -351,7 +355,7 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
messages := []Message{{Role: "user", Content: "Hello"}}
- resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{"max_tokens": 1024})
+ resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]any{"max_tokens": 1024})
if err != nil {
t.Fatalf("Chat() error: %v", err)
}
@@ -373,7 +377,7 @@ func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
return
}
- var reqBody map[string]interface{}
+ var reqBody map[string]any
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
@@ -383,27 +387,27 @@ func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
return
}
- resp := map[string]interface{}{
+ resp := map[string]any{
"id": "resp_test",
"object": "response",
"status": "completed",
- "output": []map[string]interface{}{
+ "output": []map[string]any{
{
"id": "msg_1",
"type": "message",
"role": "assistant",
"status": "completed",
- "content": []map[string]interface{}{
+ "content": []map[string]any{
{"type": "output_text", "text": "Hi from Codex!"},
},
},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"input_tokens": 4,
"output_tokens": 3,
"total_tokens": 7,
- "input_tokens_details": map[string]interface{}{"cached_tokens": 0},
- "output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
+ "input_tokens_details": map[string]any{"cached_tokens": 0},
+ "output_tokens_details": map[string]any{"reasoning_tokens": 0},
},
}
writeCompletedSSE(w, resp)
@@ -415,7 +419,7 @@ func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
messages := []Message{{Role: "user", Content: "Hello"}}
- resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{})
+ resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]any{})
if err != nil {
t.Fatalf("Chat() error: %v", err)
}
@@ -439,7 +443,7 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
return
}
- var reqBody map[string]interface{}
+ var reqBody map[string]any
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
@@ -465,27 +469,27 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
return
}
- resp := map[string]interface{}{
+ resp := map[string]any{
"id": "resp_test",
"object": "response",
"status": "completed",
- "output": []map[string]interface{}{
+ "output": []map[string]any{
{
"id": "msg_1",
"type": "message",
"role": "assistant",
"status": "completed",
- "content": []map[string]interface{}{
+ "content": []map[string]any{
{"type": "output_text", "text": "Hi from Codex!"},
},
},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"input_tokens": 8,
"output_tokens": 4,
"total_tokens": 12,
- "input_tokens_details": map[string]interface{}{"cached_tokens": 0},
- "output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
+ "input_tokens_details": map[string]any{"cached_tokens": 0},
+ "output_tokens_details": map[string]any{"reasoning_tokens": 0},
},
}
writeCompletedSSE(w, resp)
@@ -499,7 +503,7 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
}
messages := []Message{{Role: "user", Content: "Hello"}}
- resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{"temperature": 0.7})
+ resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]any{"temperature": 0.7})
if err != nil {
t.Fatalf("Chat() error: %v", err)
}
@@ -515,7 +519,7 @@ func TestCodexProvider_ChatRoundTrip_ModelFallbackFromUnsupported(t *testing.T)
return
}
- var reqBody map[string]interface{}
+ var reqBody map[string]any
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
@@ -533,27 +537,27 @@ func TestCodexProvider_ChatRoundTrip_ModelFallbackFromUnsupported(t *testing.T)
return
}
- resp := map[string]interface{}{
+ resp := map[string]any{
"id": "resp_test",
"object": "response",
"status": "completed",
- "output": []map[string]interface{}{
+ "output": []map[string]any{
{
"id": "msg_1",
"type": "message",
"role": "assistant",
"status": "completed",
- "content": []map[string]interface{}{
+ "content": []map[string]any{
{"type": "output_text", "text": "Hi from Codex!"},
},
},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"input_tokens": 8,
"output_tokens": 4,
"total_tokens": 12,
- "input_tokens_details": map[string]interface{}{"cached_tokens": 0},
- "output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
+ "input_tokens_details": map[string]any{"cached_tokens": 0},
+ "output_tokens_details": map[string]any{"reasoning_tokens": 0},
},
}
writeCompletedSSE(w, resp)
@@ -588,7 +592,12 @@ func TestResolveCodexModel(t *testing.T) {
wantFallback bool
}{
{name: "empty", input: "", wantModel: codexDefaultModel, wantFallback: true},
- {name: "unsupported namespace", input: "anthropic/claude-3.5", wantModel: codexDefaultModel, wantFallback: true},
+ {
+ name: "unsupported namespace",
+ input: "anthropic/claude-3.5",
+ wantModel: codexDefaultModel,
+ wantFallback: true,
+ },
{name: "non-openai prefixed", input: "glm-4.7", wantModel: codexDefaultModel, wantFallback: true},
{name: "openai prefix", input: "openai/gpt-5.2", wantModel: "gpt-5.2", wantFallback: false},
{name: "direct gpt", input: "gpt-4o", wantModel: "gpt-4o", wantFallback: false},
@@ -622,8 +631,8 @@ func createOpenAITestClient(baseURL, token, accountID string) *openai.Client {
return &c
}
-func writeCompletedSSE(w http.ResponseWriter, response map[string]interface{}) {
- event := map[string]interface{}{
+func writeCompletedSSE(w http.ResponseWriter, response map[string]any) {
+ event := map[string]any{
"type": "response.completed",
"sequence_number": 1,
"response": response,
diff --git a/pkg/providers/fallback.go b/pkg/providers/fallback.go
index 9b07f9153..ecd451ec9 100644
--- a/pkg/providers/fallback.go
+++ b/pkg/providers/fallback.go
@@ -110,7 +110,11 @@ func (fc *FallbackChain) Execute(
Model: candidate.Model,
Skipped: true,
Reason: FailoverRateLimit,
- Error: fmt.Errorf("provider %s in cooldown (%s remaining)", candidate.Provider, remaining.Round(time.Second)),
+ Error: fmt.Errorf(
+ "provider %s in cooldown (%s remaining)",
+ candidate.Provider,
+ remaining.Round(time.Second),
+ ),
})
continue
}
diff --git a/pkg/providers/fallback_test.go b/pkg/providers/fallback_test.go
index ea81e0d48..e872c672e 100644
--- a/pkg/providers/fallback_test.go
+++ b/pkg/providers/fallback_test.go
@@ -462,7 +462,13 @@ func TestResolveCandidates_EmptyPrimary(t *testing.T) {
func TestFallbackExhaustedError_Message(t *testing.T) {
e := &FallbackExhaustedError{
Attempts: []FallbackAttempt{
- {Provider: "openai", Model: "gpt-4", Error: errors.New("rate limited"), Reason: FailoverRateLimit, Duration: 500 * time.Millisecond},
+ {
+ Provider: "openai",
+ Model: "gpt-4",
+ Error: errors.New("rate limited"),
+ Reason: FailoverRateLimit,
+ Duration: 500 * time.Millisecond,
+ },
{Provider: "anthropic", Model: "claude", Skipped: true},
},
}
diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go
index 5058819f5..6124881f7 100644
--- a/pkg/providers/github_copilot_provider.go
+++ b/pkg/providers/github_copilot_provider.go
@@ -2,10 +2,9 @@ package providers
import (
"context"
+ "encoding/json"
"fmt"
- json "encoding/json"
-
copilot "github.com/github/copilot-sdk/go"
)
@@ -17,7 +16,6 @@ type GitHubCopilotProvider struct {
}
func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) {
-
var session *copilot.Session
if connectMode == "" {
connectMode = "grpc"
@@ -25,13 +23,15 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*Gi
switch connectMode {
case "stdio":
- //todo
+ // todo
case "grpc":
client := copilot.NewClient(&copilot.ClientOptions{
CLIUrl: uri,
})
if err := client.Start(context.Background()); err != nil {
- return nil, fmt.Errorf("Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details")
+ return nil, fmt.Errorf(
+ "Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details",
+ )
}
defer client.Stop()
session, _ = client.CreateSession(context.Background(), &copilot.SessionConfig{
@@ -49,7 +49,9 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*Gi
}
// Chat sends a chat request to GitHub Copilot
-func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *GitHubCopilotProvider) Chat(
+ ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
+) (*LLMResponse, error) {
type tempMessage struct {
Role string `json:"role"`
Content string `json:"content"`
@@ -73,10 +75,8 @@ func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, to
FinishReason: "stop",
Content: content,
}, nil
-
}
func (p *GitHubCopilotProvider) GetDefaultModel() string {
-
return "gpt-4.1"
}
diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go
index eeaa9690a..d0c4344f3 100644
--- a/pkg/providers/http_provider.go
+++ b/pkg/providers/http_provider.go
@@ -28,7 +28,13 @@ func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField st
}
}
-func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *HTTPProvider) Chat(
+ ctx context.Context,
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+) (*LLMResponse, error) {
return p.delegate.Chat(ctx, messages, tools, model, options)
}
diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go
index 6bc43a470..b8528953a 100644
--- a/pkg/providers/openai_compat/provider.go
+++ b/pkg/providers/openai_compat/provider.go
@@ -15,15 +15,17 @@ import (
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
)
-type ToolCall = protocoltypes.ToolCall
-type FunctionCall = protocoltypes.FunctionCall
-type LLMResponse = protocoltypes.LLMResponse
-type UsageInfo = protocoltypes.UsageInfo
-type Message = protocoltypes.Message
-type ToolDefinition = protocoltypes.ToolDefinition
-type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
-type ExtraContent = protocoltypes.ExtraContent
-type GoogleExtra = protocoltypes.GoogleExtra
+type (
+ ToolCall = protocoltypes.ToolCall
+ FunctionCall = protocoltypes.FunctionCall
+ LLMResponse = protocoltypes.LLMResponse
+ UsageInfo = protocoltypes.UsageInfo
+ Message = protocoltypes.Message
+ ToolDefinition = protocoltypes.ToolDefinition
+ ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
+ ExtraContent = protocoltypes.ExtraContent
+ GoogleExtra = protocoltypes.GoogleExtra
+)
type Provider struct {
apiKey string
@@ -60,14 +62,20 @@ func NewProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField string
}
}
-func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+func (p *Provider) Chat(
+ ctx context.Context,
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+) (*LLMResponse, error) {
if p.apiBase == "" {
return nil, fmt.Errorf("API base not configured")
}
model = normalizeModel(model, p.apiBase)
- requestBody := map[string]interface{}{
+ requestBody := map[string]any{
"model": model,
"messages": messages,
}
@@ -83,7 +91,8 @@ func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDef
if fieldName == "" {
// Fallback: detect from model name for backward compatibility
lowerModel := strings.ToLower(model)
- if strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "o1") || strings.Contains(lowerModel, "gpt-5") {
+ if strings.Contains(lowerModel, "glm") || strings.Contains(lowerModel, "o1") ||
+ strings.Contains(lowerModel, "gpt-5") {
fieldName = "max_completion_tokens"
} else {
fieldName = "max_tokens"
@@ -173,7 +182,7 @@ func parseResponse(body []byte) (*LLMResponse, error) {
choice := apiResponse.Choices[0]
toolCalls := make([]ToolCall, 0, len(choice.Message.ToolCalls))
for _, tc := range choice.Message.ToolCalls {
- arguments := make(map[string]interface{})
+ arguments := make(map[string]any)
name := ""
// Extract thought_signature from Gemini/Google-specific extra content
@@ -238,7 +247,7 @@ func normalizeModel(model, apiBase string) string {
}
}
-func asInt(v interface{}) (int, bool) {
+func asInt(v any) (int, bool) {
switch val := v.(type) {
case int:
return val, true
@@ -253,7 +262,7 @@ func asInt(v interface{}) (int, bool) {
}
}
-func asFloat(v interface{}) (float64, bool) {
+func asFloat(v any) (float64, bool) {
switch val := v.(type) {
case float64:
return val, true
diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go
index 94779b39c..42f9d42ab 100644
--- a/pkg/providers/openai_compat/provider_test.go
+++ b/pkg/providers/openai_compat/provider_test.go
@@ -9,7 +9,7 @@ import (
)
func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
- var requestBody map[string]interface{}
+ var requestBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/chat/completions" {
@@ -20,10 +20,10 @@ func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- resp := map[string]interface{}{
- "choices": []map[string]interface{}{
+ resp := map[string]any{
+ "choices": []map[string]any{
{
- "message": map[string]interface{}{"content": "ok"},
+ "message": map[string]any{"content": "ok"},
"finish_reason": "stop",
},
},
@@ -34,7 +34,13 @@ func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
defer server.Close()
p := NewProvider("key", server.URL, "")
- _, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "glm-4.7", map[string]interface{}{"max_tokens": 1234})
+ _, err := p.Chat(
+ t.Context(),
+ []Message{{Role: "user", Content: "hi"}},
+ nil,
+ "glm-4.7",
+ map[string]any{"max_tokens": 1234},
+ )
if err != nil {
t.Fatalf("Chat() error = %v", err)
}
@@ -49,16 +55,16 @@ func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) {
func TestProviderChat_ParsesToolCalls(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- resp := map[string]interface{}{
- "choices": []map[string]interface{}{
+ resp := map[string]any{
+ "choices": []map[string]any{
{
- "message": map[string]interface{}{
+ "message": map[string]any{
"content": "",
- "tool_calls": []map[string]interface{}{
+ "tool_calls": []map[string]any{
{
"id": "call_1",
"type": "function",
- "function": map[string]interface{}{
+ "function": map[string]any{
"name": "get_weather",
"arguments": "{\"city\":\"SF\"}",
},
@@ -68,7 +74,7 @@ func TestProviderChat_ParsesToolCalls(t *testing.T) {
"finish_reason": "tool_calls",
},
},
- "usage": map[string]interface{}{
+ "usage": map[string]any{
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
@@ -109,17 +115,17 @@ func TestProviderChat_HTTPError(t *testing.T) {
}
func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testing.T) {
- var requestBody map[string]interface{}
+ var requestBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- resp := map[string]interface{}{
- "choices": []map[string]interface{}{
+ resp := map[string]any{
+ "choices": []map[string]any{
{
- "message": map[string]interface{}{"content": "ok"},
+ "message": map[string]any{"content": "ok"},
"finish_reason": "stop",
},
},
@@ -135,7 +141,7 @@ func TestProviderChat_StripsMoonshotPrefixAndNormalizesKimiTemperature(t *testin
[]Message{{Role: "user", Content: "hi"}},
nil,
"moonshot/kimi-k2.5",
- map[string]interface{}{"temperature": 0.3},
+ map[string]any{"temperature": 0.3},
)
if err != nil {
t.Fatalf("Chat() error = %v", err)
@@ -174,17 +180,17 @@ func TestProviderChat_StripsGroqAndOllamaPrefixes(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- var requestBody map[string]interface{}
+ var requestBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- resp := map[string]interface{}{
- "choices": []map[string]interface{}{
+ resp := map[string]any{
+ "choices": []map[string]any{
{
- "message": map[string]interface{}{"content": "ok"},
+ "message": map[string]any{"content": "ok"},
"finish_reason": "stop",
},
},
@@ -227,17 +233,17 @@ func TestProvider_ProxyConfigured(t *testing.T) {
}
func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) {
- var requestBody map[string]interface{}
+ var requestBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- resp := map[string]interface{}{
- "choices": []map[string]interface{}{
+ resp := map[string]any{
+ "choices": []map[string]any{
{
- "message": map[string]interface{}{"content": "ok"},
+ "message": map[string]any{"content": "ok"},
"finish_reason": "stop",
},
},
@@ -253,7 +259,7 @@ func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) {
[]Message{{Role: "user", Content: "hi"}},
nil,
"gpt-4o",
- map[string]interface{}{"max_tokens": float64(512), "temperature": 1},
+ map[string]any{"max_tokens": float64(512), "temperature": 1},
)
if err != nil {
t.Fatalf("Chat() error = %v", err)
diff --git a/pkg/providers/protocoltypes/types.go b/pkg/providers/protocoltypes/types.go
index b7e7062b9..3a089ca47 100644
--- a/pkg/providers/protocoltypes/types.go
+++ b/pkg/providers/protocoltypes/types.go
@@ -1,13 +1,13 @@
package protocoltypes
type ToolCall struct {
- ID string `json:"id"`
- Type string `json:"type,omitempty"`
- Function *FunctionCall `json:"function,omitempty"`
- Name string `json:"name,omitempty"`
- Arguments map[string]interface{} `json:"arguments,omitempty"`
- ThoughtSignature string `json:"-"` // Internal use only
- ExtraContent *ExtraContent `json:"extra_content,omitempty"`
+ ID string `json:"id"`
+ Type string `json:"type,omitempty"`
+ Function *FunctionCall `json:"function,omitempty"`
+ Name string `json:"name,omitempty"`
+ Arguments map[string]any `json:"arguments,omitempty"`
+ ThoughtSignature string `json:"-"` // Internal use only
+ ExtraContent *ExtraContent `json:"extra_content,omitempty"`
}
type ExtraContent struct {
@@ -50,7 +50,7 @@ type ToolDefinition struct {
}
type ToolFunctionDefinition struct {
- Name string `json:"name"`
- Description string `json:"description"`
- Parameters map[string]interface{} `json:"parameters"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Parameters map[string]any `json:"parameters"`
}
diff --git a/pkg/providers/tool_call_extract.go b/pkg/providers/tool_call_extract.go
index 97a219283..7ddea0e99 100644
--- a/pkg/providers/tool_call_extract.go
+++ b/pkg/providers/tool_call_extract.go
@@ -38,7 +38,7 @@ func extractToolCallsFromText(text string) []ToolCall {
var result []ToolCall
for _, tc := range wrapper.ToolCalls {
- var args map[string]interface{}
+ var args map[string]any
json.Unmarshal([]byte(tc.Function.Arguments), &args)
result = append(result, ToolCall{
diff --git a/pkg/providers/toolcall_utils.go b/pkg/providers/toolcall_utils.go
index c7c35ef42..49218b1b1 100644
--- a/pkg/providers/toolcall_utils.go
+++ b/pkg/providers/toolcall_utils.go
@@ -20,12 +20,12 @@ func NormalizeToolCall(tc ToolCall) ToolCall {
// Ensure Arguments is not nil
if normalized.Arguments == nil {
- normalized.Arguments = map[string]interface{}{}
+ normalized.Arguments = map[string]any{}
}
// Parse Arguments from Function.Arguments if not already set
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
- var parsed map[string]interface{}
+ var parsed map[string]any
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
normalized.Arguments = parsed
}
diff --git a/pkg/providers/types.go b/pkg/providers/types.go
index e783e6348..f711e7803 100644
--- a/pkg/providers/types.go
+++ b/pkg/providers/types.go
@@ -7,18 +7,26 @@ import (
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
)
-type ToolCall = protocoltypes.ToolCall
-type FunctionCall = protocoltypes.FunctionCall
-type LLMResponse = protocoltypes.LLMResponse
-type UsageInfo = protocoltypes.UsageInfo
-type Message = protocoltypes.Message
-type ToolDefinition = protocoltypes.ToolDefinition
-type ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
-type ExtraContent = protocoltypes.ExtraContent
-type GoogleExtra = protocoltypes.GoogleExtra
+type (
+ ToolCall = protocoltypes.ToolCall
+ FunctionCall = protocoltypes.FunctionCall
+ LLMResponse = protocoltypes.LLMResponse
+ UsageInfo = protocoltypes.UsageInfo
+ Message = protocoltypes.Message
+ ToolDefinition = protocoltypes.ToolDefinition
+ ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
+ ExtraContent = protocoltypes.ExtraContent
+ GoogleExtra = protocoltypes.GoogleExtra
+)
type LLMProvider interface {
- Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error)
+ Chat(
+ ctx context.Context,
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+ ) (*LLMResponse, error)
GetDefaultModel() string
}
diff --git a/pkg/session/manager.go b/pkg/session/manager.go
index 12bf33df0..08f0b0ad2 100644
--- a/pkg/session/manager.go
+++ b/pkg/session/manager.go
@@ -32,7 +32,7 @@ func NewSessionManager(storage string) *SessionManager {
}
if storage != "" {
- os.MkdirAll(storage, 0755)
+ os.MkdirAll(storage, 0o755)
sm.loadSessions()
}
@@ -214,7 +214,7 @@ func (sm *SessionManager) Save(key string) error {
_ = tmpFile.Close()
return err
}
- if err := tmpFile.Chmod(0644); err != nil {
+ if err := tmpFile.Chmod(0o644); err != nil {
_ = tmpFile.Close()
return err
}
diff --git a/pkg/skills/clawhub_registry.go b/pkg/skills/clawhub_registry.go
index e2a940afd..f78197bbe 100644
--- a/pkg/skills/clawhub_registry.go
+++ b/pkg/skills/clawhub_registry.go
@@ -214,7 +214,10 @@ func (c *ClawHubRegistry) GetSkillMeta(ctx context.Context, slug string) (*Skill
// DownloadAndInstall fetches metadata (with fallback), resolves version,
// downloads the skill ZIP, and extracts it to targetDir.
// Returns an InstallResult for the caller to use for moderation decisions.
-func (c *ClawHubRegistry) DownloadAndInstall(ctx context.Context, slug, version, targetDir string) (*InstallResult, error) {
+func (c *ClawHubRegistry) DownloadAndInstall(
+ ctx context.Context,
+ slug, version, targetDir string,
+) (*InstallResult, error) {
if err := utils.ValidateSkillIdentifier(slug); err != nil {
return nil, fmt.Errorf("invalid slug %q: error: %s", slug, err.Error())
}
diff --git a/pkg/skills/clawhub_registry_test.go b/pkg/skills/clawhub_registry_test.go
index d12e19504..65ee638da 100644
--- a/pkg/skills/clawhub_registry_test.go
+++ b/pkg/skills/clawhub_registry_test.go
@@ -11,9 +11,10 @@ import (
"path/filepath"
"testing"
- "github.com/sipeed/picoclaw/pkg/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/sipeed/picoclaw/pkg/utils"
)
func newTestRegistry(serverURL, authToken string) *ClawHubRegistry {
@@ -162,7 +163,7 @@ func TestExtractZipPathTraversal(t *testing.T) {
// Write to temp file for extractZipFile.
tmpZip := filepath.Join(t.TempDir(), "bad.zip")
- require.NoError(t, os.WriteFile(tmpZip, buf.Bytes(), 0644))
+ require.NoError(t, os.WriteFile(tmpZip, buf.Bytes(), 0o644))
tmpDir := t.TempDir()
err = utils.ExtractZipFile(tmpZip, tmpDir)
@@ -179,7 +180,7 @@ func TestExtractZipWithSubdirectories(t *testing.T) {
// Write to temp file for extractZipFile.
tmpZip := filepath.Join(t.TempDir(), "test.zip")
- require.NoError(t, os.WriteFile(tmpZip, zipBuf, 0644))
+ require.NoError(t, os.WriteFile(tmpZip, zipBuf, 0o644))
tmpDir := t.TempDir()
targetDir := filepath.Join(tmpDir, "my-skill")
diff --git a/pkg/skills/installer.go b/pkg/skills/installer.go
index 0856254e8..3210509df 100644
--- a/pkg/skills/installer.go
+++ b/pkg/skills/installer.go
@@ -59,12 +59,12 @@ func (si *SkillInstaller) InstallFromGitHub(ctx context.Context, repo string) er
return fmt.Errorf("failed to read response: %w", err)
}
- if err := os.MkdirAll(skillDir, 0755); err != nil {
+ if err := os.MkdirAll(skillDir, 0o755); err != nil {
return fmt.Errorf("failed to create skill directory: %w", err)
}
skillPath := filepath.Join(skillDir, "SKILL.md")
- if err := os.WriteFile(skillPath, body, 0644); err != nil {
+ if err := os.WriteFile(skillPath, body, 0o644); err != nil {
return fmt.Errorf("failed to write skill file: %w", err)
}
diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go
index bb0abbdcc..eb0d5f322 100644
--- a/pkg/skills/loader.go
+++ b/pkg/skills/loader.go
@@ -254,7 +254,7 @@ func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata {
content, err := os.ReadFile(skillPath)
if err != nil {
logger.WarnCF("skills", "Failed to read skill metadata",
- map[string]interface{}{
+ map[string]any{
"skill_path": skillPath,
"error": err.Error(),
})
diff --git a/pkg/skills/loader_test.go b/pkg/skills/loader_test.go
index efadcdbf2..aca901d33 100644
--- a/pkg/skills/loader_test.go
+++ b/pkg/skills/loader_test.go
@@ -117,8 +117,20 @@ func TestExtractFrontmatter(t *testing.T) {
// Parse YAML to get name and description (parseSimpleYAML now handles all line ending types)
yamlMeta := sl.parseSimpleYAML(frontmatter)
- assert.Equal(t, tc.expectedName, yamlMeta["name"], "Name should be correctly parsed from frontmatter with %s line endings", tc.lineEndingType)
- assert.Equal(t, tc.expectedDesc, yamlMeta["description"], "Description should be correctly parsed from frontmatter with %s line endings", tc.lineEndingType)
+ assert.Equal(
+ t,
+ tc.expectedName,
+ yamlMeta["name"],
+ "Name should be correctly parsed from frontmatter with %s line endings",
+ tc.lineEndingType,
+ )
+ assert.Equal(
+ t,
+ tc.expectedDesc,
+ yamlMeta["description"],
+ "Description should be correctly parsed from frontmatter with %s line endings",
+ tc.lineEndingType,
+ )
})
}
}
@@ -173,7 +185,13 @@ func TestStripFrontmatter(t *testing.T) {
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
result := sl.stripFrontmatter(tc.content)
- assert.Equal(t, tc.expectedContent, result, "Frontmatter should be stripped correctly for %s", tc.lineEndingType)
+ assert.Equal(
+ t,
+ tc.expectedContent,
+ result,
+ "Frontmatter should be stripped correctly for %s",
+ tc.lineEndingType,
+ )
})
}
}
diff --git a/pkg/skills/registry_test.go b/pkg/skills/registry_test.go
index daecd5a59..a4694bd43 100644
--- a/pkg/skills/registry_test.go
+++ b/pkg/skills/registry_test.go
@@ -6,8 +6,9 @@ import (
"testing"
"time"
- "github.com/sipeed/picoclaw/pkg/utils"
"github.com/stretchr/testify/assert"
+
+ "github.com/sipeed/picoclaw/pkg/utils"
)
// mockRegistry is a test double implementing SkillRegistry.
diff --git a/pkg/state/state.go b/pkg/state/state.go
index 0bb9cd497..1a92f82ed 100644
--- a/pkg/state/state.go
+++ b/pkg/state/state.go
@@ -38,7 +38,7 @@ func NewManager(workspace string) *Manager {
oldStateFile := filepath.Join(workspace, "state.json")
// Create state directory if it doesn't exist
- os.MkdirAll(stateDir, 0755)
+ os.MkdirAll(stateDir, 0o755)
sm := &Manager{
workspace: workspace,
@@ -139,7 +139,7 @@ func (sm *Manager) saveAtomic() error {
}
// Write to temp file
- if err := os.WriteFile(tempFile, data, 0644); err != nil {
+ if err := os.WriteFile(tempFile, data, 0o644); err != nil {
return fmt.Errorf("failed to write temp file: %w", err)
}
diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go
index ce3dd7215..f717a5bb4 100644
--- a/pkg/state/state_test.go
+++ b/pkg/state/state_test.go
@@ -98,7 +98,7 @@ func TestAtomicity_NoCorruptionOnInterrupt(t *testing.T) {
// Simulate a crash scenario by manually creating a corrupted temp file
tempFile := filepath.Join(tmpDir, "state", "state.json.tmp")
- err = os.WriteFile(tempFile, []byte("corrupted data"), 0644)
+ err = os.WriteFile(tempFile, []byte("corrupted data"), 0o644)
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
diff --git a/pkg/tools/base.go b/pkg/tools/base.go
index b13174633..770d8cb04 100644
--- a/pkg/tools/base.go
+++ b/pkg/tools/base.go
@@ -6,8 +6,8 @@ import "context"
type Tool interface {
Name() string
Description() string
- Parameters() map[string]interface{}
- Execute(ctx context.Context, args map[string]interface{}) *ToolResult
+ Parameters() map[string]any
+ Execute(ctx context.Context, args map[string]any) *ToolResult
}
// ContextualTool is an optional interface that tools can implement
@@ -69,10 +69,10 @@ type AsyncTool interface {
SetCallback(cb AsyncCallback)
}
-func ToolToSchema(tool Tool) map[string]interface{} {
- return map[string]interface{}{
+func ToolToSchema(tool Tool) map[string]any {
+ return map[string]any{
"type": "function",
- "function": map[string]interface{}{
+ "function": map[string]any{
"name": tool.Name(),
"description": tool.Description(),
"parameters": tool.Parameters(),
diff --git a/pkg/tools/cron.go b/pkg/tools/cron.go
index e2764d8ac..562fffc84 100644
--- a/pkg/tools/cron.go
+++ b/pkg/tools/cron.go
@@ -30,7 +30,10 @@ type CronTool struct {
// NewCronTool creates a new CronTool
// execTimeout: 0 means no timeout, >0 sets the timeout duration
-func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
+func NewCronTool(
+ cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool,
+ execTimeout time.Duration, config *config.Config,
+) *CronTool {
execTool := NewExecToolWithConfig(workspace, restrict, config)
execTool.SetTimeout(execTimeout)
return &CronTool{
@@ -52,40 +55,40 @@ func (t *CronTool) Description() string {
}
// Parameters returns the tool parameters schema
-func (t *CronTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *CronTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "action": map[string]interface{}{
+ "properties": map[string]any{
+ "action": map[string]any{
"type": "string",
"enum": []string{"add", "list", "remove", "enable", "disable"},
"description": "Action to perform. Use 'add' when user wants to schedule a reminder or task.",
},
- "message": map[string]interface{}{
+ "message": map[string]any{
"type": "string",
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.",
},
- "command": map[string]interface{}{
+ "command": map[string]any{
"type": "string",
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
},
- "at_seconds": map[string]interface{}{
+ "at_seconds": map[string]any{
"type": "integer",
"description": "One-time reminder: seconds from now when to trigger (e.g., 600 for 10 minutes later). Use this for one-time reminders like 'remind me in 10 minutes'.",
},
- "every_seconds": map[string]interface{}{
+ "every_seconds": map[string]any{
"type": "integer",
"description": "Recurring interval in seconds (e.g., 3600 for every hour). Use this ONLY for recurring tasks like 'every 2 hours' or 'daily reminder'.",
},
- "cron_expr": map[string]interface{}{
+ "cron_expr": map[string]any{
"type": "string",
"description": "Cron expression for complex recurring schedules (e.g., '0 9 * * *' for daily at 9am). Use this for complex recurring schedules.",
},
- "job_id": map[string]interface{}{
+ "job_id": map[string]any{
"type": "string",
"description": "Job ID (for remove/enable/disable)",
},
- "deliver": map[string]interface{}{
+ "deliver": map[string]any{
"type": "boolean",
"description": "If true, send message directly to channel. If false, let agent process message (for complex tasks). Default: true",
},
@@ -103,7 +106,7 @@ func (t *CronTool) SetContext(channel, chatID string) {
}
// Execute runs the tool with the given arguments
-func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *CronTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
action, ok := args["action"].(string)
if !ok {
return ErrorResult("action is required")
@@ -125,7 +128,7 @@ func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *To
}
}
-func (t *CronTool) addJob(args map[string]interface{}) *ToolResult {
+func (t *CronTool) addJob(args map[string]any) *ToolResult {
t.mu.RLock()
channel := t.channel
chatID := t.chatID
@@ -233,7 +236,7 @@ func (t *CronTool) listJobs() *ToolResult {
return SilentResult(result)
}
-func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
+func (t *CronTool) removeJob(args map[string]any) *ToolResult {
jobID, ok := args["job_id"].(string)
if !ok || jobID == "" {
return ErrorResult("job_id is required for remove")
@@ -245,7 +248,7 @@ func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
}
-func (t *CronTool) enableJob(args map[string]interface{}, enable bool) *ToolResult {
+func (t *CronTool) enableJob(args map[string]any, enable bool) *ToolResult {
jobID, ok := args["job_id"].(string)
if !ok || jobID == "" {
return ErrorResult("job_id is required for enable/disable")
@@ -279,7 +282,7 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
// Execute command if present
if job.Payload.Command != "" {
- args := map[string]interface{}{
+ args := map[string]any{
"command": job.Payload.Command,
}
@@ -320,7 +323,6 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
channel,
chatID,
)
-
if err != nil {
return fmt.Sprintf("Error: %v", err)
}
diff --git a/pkg/tools/edit.go b/pkg/tools/edit.go
index 1e7c33b45..c28ca6ca2 100644
--- a/pkg/tools/edit.go
+++ b/pkg/tools/edit.go
@@ -30,19 +30,19 @@ func (t *EditFileTool) Description() string {
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file."
}
-func (t *EditFileTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *EditFileTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
+ "properties": map[string]any{
+ "path": map[string]any{
"type": "string",
"description": "The file path to edit",
},
- "old_text": map[string]interface{}{
+ "old_text": map[string]any{
"type": "string",
"description": "The exact text to find and replace",
},
- "new_text": map[string]interface{}{
+ "new_text": map[string]any{
"type": "string",
"description": "The text to replace with",
},
@@ -51,7 +51,7 @@ func (t *EditFileTool) Parameters() map[string]interface{} {
}
}
-func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *EditFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -72,7 +72,7 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{})
return ErrorResult(err.Error())
}
- if _, err := os.Stat(resolvedPath); os.IsNotExist(err) {
+ if _, err = os.Stat(resolvedPath); os.IsNotExist(err) {
return ErrorResult(fmt.Sprintf("file not found: %s", path))
}
@@ -89,12 +89,14 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{})
count := strings.Count(contentStr, oldText)
if count > 1 {
- return ErrorResult(fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count))
+ return ErrorResult(
+ fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count),
+ )
}
newContent := strings.Replace(contentStr, oldText, newText, 1)
- if err := os.WriteFile(resolvedPath, []byte(newContent), 0644); err != nil {
+ if err := os.WriteFile(resolvedPath, []byte(newContent), 0o644); err != nil {
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
}
@@ -118,15 +120,15 @@ func (t *AppendFileTool) Description() string {
return "Append content to the end of a file"
}
-func (t *AppendFileTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *AppendFileTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
+ "properties": map[string]any{
+ "path": map[string]any{
"type": "string",
"description": "The file path to append to",
},
- "content": map[string]interface{}{
+ "content": map[string]any{
"type": "string",
"description": "The content to append",
},
@@ -135,7 +137,7 @@ func (t *AppendFileTool) Parameters() map[string]interface{} {
}
}
-func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *AppendFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -151,7 +153,7 @@ func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{
return ErrorResult(err.Error())
}
- f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to open file: %v", err))
}
diff --git a/pkg/tools/edit_test.go b/pkg/tools/edit_test.go
index c4c02772d..6780dd9f6 100644
--- a/pkg/tools/edit_test.go
+++ b/pkg/tools/edit_test.go
@@ -12,11 +12,11 @@ import (
func TestEditTool_EditFile_Success(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
- os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0644)
+ os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0o644)
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"old_text": "World",
"new_text": "Universe",
@@ -60,7 +60,7 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"old_text": "old",
"new_text": "new",
@@ -83,11 +83,11 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
- os.WriteFile(testFile, []byte("Hello World"), 0644)
+ os.WriteFile(testFile, []byte("Hello World"), 0o644)
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"old_text": "Goodbye",
"new_text": "Hello",
@@ -110,11 +110,11 @@ func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
func TestEditTool_EditFile_MultipleMatches(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
- os.WriteFile(testFile, []byte("test test test"), 0644)
+ os.WriteFile(testFile, []byte("test test test"), 0o644)
tool := NewEditFileTool(tmpDir, true)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"old_text": "test",
"new_text": "done",
@@ -138,11 +138,11 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
tmpDir := t.TempDir()
otherDir := t.TempDir()
testFile := filepath.Join(otherDir, "test.txt")
- os.WriteFile(testFile, []byte("content"), 0644)
+ os.WriteFile(testFile, []byte("content"), 0o644)
tool := NewEditFileTool(tmpDir, true) // Restrict to tmpDir
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"old_text": "content",
"new_text": "new",
@@ -165,7 +165,7 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
func TestEditTool_EditFile_MissingPath(t *testing.T) {
tool := NewEditFileTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"old_text": "old",
"new_text": "new",
}
@@ -182,7 +182,7 @@ func TestEditTool_EditFile_MissingPath(t *testing.T) {
func TestEditTool_EditFile_MissingOldText(t *testing.T) {
tool := NewEditFileTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": "/tmp/test.txt",
"new_text": "new",
}
@@ -199,7 +199,7 @@ func TestEditTool_EditFile_MissingOldText(t *testing.T) {
func TestEditTool_EditFile_MissingNewText(t *testing.T) {
tool := NewEditFileTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": "/tmp/test.txt",
"old_text": "old",
}
@@ -216,11 +216,11 @@ func TestEditTool_EditFile_MissingNewText(t *testing.T) {
func TestEditTool_AppendFile_Success(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
- os.WriteFile(testFile, []byte("Initial content"), 0644)
+ os.WriteFile(testFile, []byte("Initial content"), 0o644)
tool := NewAppendFileTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"content": "\nAppended content",
}
@@ -260,7 +260,7 @@ func TestEditTool_AppendFile_Success(t *testing.T) {
func TestEditTool_AppendFile_MissingPath(t *testing.T) {
tool := NewAppendFileTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"content": "test",
}
@@ -276,7 +276,7 @@ func TestEditTool_AppendFile_MissingPath(t *testing.T) {
func TestEditTool_AppendFile_MissingContent(t *testing.T) {
tool := NewAppendFileTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": "/tmp/test.txt",
}
diff --git a/pkg/tools/filesystem.go b/pkg/tools/filesystem.go
index 09063ea0a..1bf50906e 100644
--- a/pkg/tools/filesystem.go
+++ b/pkg/tools/filesystem.go
@@ -34,17 +34,19 @@ func validatePath(path, workspace string, restrict bool) (string, error) {
return "", fmt.Errorf("access denied: path is outside the workspace")
}
+ var resolved string
workspaceReal := absWorkspace
- if resolved, err := filepath.EvalSymlinks(absWorkspace); err == nil {
+ if resolved, err = filepath.EvalSymlinks(absWorkspace); err == nil {
workspaceReal = resolved
}
- if resolved, err := filepath.EvalSymlinks(absPath); err == nil {
+ if resolved, err = filepath.EvalSymlinks(absPath); err == nil {
if !isWithinWorkspace(resolved, workspaceReal) {
return "", fmt.Errorf("access denied: symlink resolves outside workspace")
}
} else if os.IsNotExist(err) {
- if parentResolved, err := resolveExistingAncestor(filepath.Dir(absPath)); err == nil {
+ var parentResolved string
+ if parentResolved, err = resolveExistingAncestor(filepath.Dir(absPath)); err == nil {
if !isWithinWorkspace(parentResolved, workspaceReal) {
return "", fmt.Errorf("access denied: symlink resolves outside workspace")
}
@@ -94,11 +96,11 @@ func (t *ReadFileTool) Description() string {
return "Read the contents of a file"
}
-func (t *ReadFileTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *ReadFileTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
+ "properties": map[string]any{
+ "path": map[string]any{
"type": "string",
"description": "Path to the file to read",
},
@@ -107,7 +109,7 @@ func (t *ReadFileTool) Parameters() map[string]interface{} {
}
}
-func (t *ReadFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -143,15 +145,15 @@ func (t *WriteFileTool) Description() string {
return "Write content to a file"
}
-func (t *WriteFileTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *WriteFileTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
+ "properties": map[string]any{
+ "path": map[string]any{
"type": "string",
"description": "Path to the file to write",
},
- "content": map[string]interface{}{
+ "content": map[string]any{
"type": "string",
"description": "Content to write to the file",
},
@@ -160,7 +162,7 @@ func (t *WriteFileTool) Parameters() map[string]interface{} {
}
}
-func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
return ErrorResult("path is required")
@@ -177,11 +179,11 @@ func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}
}
dir := filepath.Dir(resolvedPath)
- if err := os.MkdirAll(dir, 0755); err != nil {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
return ErrorResult(fmt.Sprintf("failed to create directory: %v", err))
}
- if err := os.WriteFile(resolvedPath, []byte(content), 0644); err != nil {
+ if err := os.WriteFile(resolvedPath, []byte(content), 0o644); err != nil {
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
}
@@ -205,11 +207,11 @@ func (t *ListDirTool) Description() string {
return "List files and directories in a path"
}
-func (t *ListDirTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *ListDirTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
+ "properties": map[string]any{
+ "path": map[string]any{
"type": "string",
"description": "Path to list",
},
@@ -218,7 +220,7 @@ func (t *ListDirTool) Parameters() map[string]interface{} {
}
}
-func (t *ListDirTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
path, ok := args["path"].(string)
if !ok {
path = "."
diff --git a/pkg/tools/filesystem_test.go b/pkg/tools/filesystem_test.go
index 958036419..5daa3dcea 100644
--- a/pkg/tools/filesystem_test.go
+++ b/pkg/tools/filesystem_test.go
@@ -12,11 +12,11 @@ import (
func TestFilesystemTool_ReadFile_Success(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
- os.WriteFile(testFile, []byte("test content"), 0644)
+ os.WriteFile(testFile, []byte("test content"), 0o644)
tool := &ReadFileTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
}
@@ -43,7 +43,7 @@ func TestFilesystemTool_ReadFile_Success(t *testing.T) {
func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
tool := &ReadFileTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": "/nonexistent_file_12345.txt",
}
@@ -64,7 +64,7 @@ func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
func TestFilesystemTool_ReadFile_MissingPath(t *testing.T) {
tool := &ReadFileTool{}
ctx := context.Background()
- args := map[string]interface{}{}
+ args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -86,7 +86,7 @@ func TestFilesystemTool_WriteFile_Success(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"content": "hello world",
}
@@ -125,7 +125,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": testFile,
"content": "test",
}
@@ -151,7 +151,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"content": "test",
}
@@ -167,7 +167,7 @@ func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
tool := &WriteFileTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": "/tmp/test.txt",
}
@@ -179,7 +179,8 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
}
// Should mention required parameter
- if !strings.Contains(result.ForLLM, "content is required") && !strings.Contains(result.ForUser, "content is required") {
+ if !strings.Contains(result.ForLLM, "content is required") &&
+ !strings.Contains(result.ForUser, "content is required") {
t.Errorf("Expected 'content is required' message, got ForLLM: %s", result.ForLLM)
}
}
@@ -187,13 +188,13 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
// TestFilesystemTool_ListDir_Success verifies successful directory listing
func TestFilesystemTool_ListDir_Success(t *testing.T) {
tmpDir := t.TempDir()
- os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0644)
- os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0644)
- os.Mkdir(filepath.Join(tmpDir, "subdir"), 0755)
+ os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0o644)
+ os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0o644)
+ os.Mkdir(filepath.Join(tmpDir, "subdir"), 0o755)
tool := &ListDirTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": tmpDir,
}
@@ -217,7 +218,7 @@ func TestFilesystemTool_ListDir_Success(t *testing.T) {
func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
tool := &ListDirTool{}
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"path": "/nonexistent_directory_12345",
}
@@ -238,7 +239,7 @@ func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
tool := &ListDirTool{}
ctx := context.Background()
- args := map[string]interface{}{}
+ args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -250,15 +251,14 @@ func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
// Block paths that look inside workspace but point outside via symlink.
func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
-
root := t.TempDir()
workspace := filepath.Join(root, "workspace")
- if err := os.MkdirAll(workspace, 0755); err != nil {
+ if err := os.MkdirAll(workspace, 0o755); err != nil {
t.Fatalf("failed to create workspace: %v", err)
}
secret := filepath.Join(root, "secret.txt")
- if err := os.WriteFile(secret, []byte("top secret"), 0644); err != nil {
+ if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil {
t.Fatalf("failed to write secret file: %v", err)
}
@@ -268,7 +268,7 @@ func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
}
tool := NewReadFileTool(workspace, true)
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"path": link,
})
diff --git a/pkg/tools/i2c.go b/pkg/tools/i2c.go
index abca5ec1e..0387a26d3 100644
--- a/pkg/tools/i2c.go
+++ b/pkg/tools/i2c.go
@@ -24,37 +24,37 @@ func (t *I2CTool) Description() string {
return "Interact with I2C bus devices for reading sensors and controlling peripherals. Actions: detect (list buses), scan (find devices on a bus), read (read bytes from device), write (send bytes to device). Linux only."
}
-func (t *I2CTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *I2CTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "action": map[string]interface{}{
+ "properties": map[string]any{
+ "action": map[string]any{
"type": "string",
"enum": []string{"detect", "scan", "read", "write"},
"description": "Action to perform: detect (list available I2C buses), scan (find devices on a bus), read (read bytes from a device), write (send bytes to a device)",
},
- "bus": map[string]interface{}{
+ "bus": map[string]any{
"type": "string",
"description": "I2C bus number (e.g. \"1\" for /dev/i2c-1). Required for scan/read/write.",
},
- "address": map[string]interface{}{
+ "address": map[string]any{
"type": "integer",
"description": "7-bit I2C device address (0x03-0x77). Required for read/write.",
},
- "register": map[string]interface{}{
+ "register": map[string]any{
"type": "integer",
"description": "Register address to read from or write to. If set, sends register byte before read/write.",
},
- "data": map[string]interface{}{
+ "data": map[string]any{
"type": "array",
- "items": map[string]interface{}{"type": "integer"},
+ "items": map[string]any{"type": "integer"},
"description": "Bytes to write (0-255 each). Required for write action.",
},
- "length": map[string]interface{}{
+ "length": map[string]any{
"type": "integer",
"description": "Number of bytes to read (1-256). Default: 1. Used with read action.",
},
- "confirm": map[string]interface{}{
+ "confirm": map[string]any{
"type": "boolean",
"description": "Must be true for write operations. Safety guard to prevent accidental writes.",
},
@@ -63,7 +63,7 @@ func (t *I2CTool) Parameters() map[string]interface{} {
}
}
-func (t *I2CTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *I2CTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
if runtime.GOOS != "linux" {
return ErrorResult("I2C is only supported on Linux. This tool requires /dev/i2c-* device files.")
}
@@ -95,7 +95,9 @@ func (t *I2CTool) detect() *ToolResult {
}
if len(matches) == 0 {
- return SilentResult("No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)")
+ return SilentResult(
+ "No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)",
+ )
}
type busInfo struct {
@@ -122,7 +124,7 @@ func isValidBusID(id string) bool {
}
// parseI2CAddress extracts and validates an I2C address from args
-func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
+func parseI2CAddress(args map[string]any) (int, *ToolResult) {
addrFloat, ok := args["address"].(float64)
if !ok {
return 0, ErrorResult("address is required (e.g. 0x38 for AHT20)")
@@ -135,7 +137,7 @@ func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
}
// parseI2CBus extracts and validates an I2C bus from args
-func parseI2CBus(args map[string]interface{}) (string, *ToolResult) {
+func parseI2CBus(args map[string]any) (string, *ToolResult) {
bus, ok := args["bus"].(string)
if !ok || bus == "" {
return "", ErrorResult("bus is required (e.g. \"1\" for /dev/i2c-1)")
diff --git a/pkg/tools/i2c_linux.go b/pkg/tools/i2c_linux.go
index 294f7ecbc..4eaaf8f09 100644
--- a/pkg/tools/i2c_linux.go
+++ b/pkg/tools/i2c_linux.go
@@ -74,7 +74,7 @@ func smbusProbe(fd int, addr int, hasQuick bool) bool {
// scan probes valid 7-bit addresses on a bus for connected devices.
// Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO:
// SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges.
-func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
+func (t *I2CTool) scan(args map[string]any) *ToolResult {
bus, errResult := parseI2CBus(args)
if errResult != nil {
return errResult
@@ -99,7 +99,9 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
hasReadByte := funcs&i2cFuncSmbusReadByte != 0
if !hasQuick && !hasReadByte {
- return ErrorResult(fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath))
+ return ErrorResult(
+ fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath),
+ )
}
type deviceEntry struct {
@@ -133,7 +135,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
}
- result, _ := json.MarshalIndent(map[string]interface{}{
+ result, _ := json.MarshalIndent(map[string]any{
"bus": devPath,
"devices": found,
"count": len(found),
@@ -142,7 +144,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
}
// readDevice reads bytes from an I2C device, optionally at a specific register
-func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
+func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
bus, errResult := parseI2CBus(args)
if errResult != nil {
return errResult
@@ -180,7 +182,7 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
if reg < 0 || reg > 255 {
return ErrorResult("register must be between 0x00 and 0xFF")
}
- _, err := syscall.Write(fd, []byte{byte(reg)})
+ _, err = syscall.Write(fd, []byte{byte(reg)})
if err != nil {
return ErrorResult(fmt.Sprintf("failed to write register 0x%02x: %v", reg, err))
}
@@ -201,7 +203,7 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
intBytes[i] = int(buf[i])
}
- result, _ := json.MarshalIndent(map[string]interface{}{
+ result, _ := json.MarshalIndent(map[string]any{
"bus": devPath,
"address": fmt.Sprintf("0x%02x", addr),
"bytes": intBytes,
@@ -212,10 +214,12 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
}
// writeDevice writes bytes to an I2C device, optionally at a specific register
-func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
+func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
confirm, _ := args["confirm"].(bool)
if !confirm {
- return ErrorResult("write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.")
+ return ErrorResult(
+ "write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.",
+ )
}
bus, errResult := parseI2CBus(args)
@@ -228,7 +232,7 @@ func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
return errResult
}
- dataRaw, ok := args["data"].([]interface{})
+ dataRaw, ok := args["data"].([]any)
if !ok || len(dataRaw) == 0 {
return ErrorResult("data is required for write (array of byte values 0-255)")
}
diff --git a/pkg/tools/i2c_other.go b/pkg/tools/i2c_other.go
index d1d581348..7becf8339 100644
--- a/pkg/tools/i2c_other.go
+++ b/pkg/tools/i2c_other.go
@@ -3,16 +3,16 @@
package tools
// scan is a stub for non-Linux platforms.
-func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
+func (t *I2CTool) scan(args map[string]any) *ToolResult {
return ErrorResult("I2C is only supported on Linux")
}
// readDevice is a stub for non-Linux platforms.
-func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
+func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
return ErrorResult("I2C is only supported on Linux")
}
// writeDevice is a stub for non-Linux platforms.
-func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
+func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
return ErrorResult("I2C is only supported on Linux")
}
diff --git a/pkg/tools/message.go b/pkg/tools/message.go
index abedb1316..15ef4ff73 100644
--- a/pkg/tools/message.go
+++ b/pkg/tools/message.go
@@ -26,19 +26,19 @@ func (t *MessageTool) Description() string {
return "Send a message to user on a chat channel. Use this when you want to communicate something."
}
-func (t *MessageTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *MessageTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "content": map[string]interface{}{
+ "properties": map[string]any{
+ "content": map[string]any{
"type": "string",
"description": "The message content to send",
},
- "channel": map[string]interface{}{
+ "channel": map[string]any{
"type": "string",
"description": "Optional: target channel (telegram, whatsapp, etc.)",
},
- "chat_id": map[string]interface{}{
+ "chat_id": map[string]any{
"type": "string",
"description": "Optional: target chat/user ID",
},
@@ -62,7 +62,7 @@ func (t *MessageTool) SetSendCallback(callback SendCallback) {
t.sendCallback = callback
}
-func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
content, ok := args["content"].(string)
if !ok {
return &ToolResult{ForLLM: "content is required", IsError: true}
diff --git a/pkg/tools/message_test.go b/pkg/tools/message_test.go
index 4bedbe79b..717c1117b 100644
--- a/pkg/tools/message_test.go
+++ b/pkg/tools/message_test.go
@@ -19,7 +19,7 @@ func TestMessageTool_Execute_Success(t *testing.T) {
})
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"content": "Hello, world!",
}
@@ -70,7 +70,7 @@ func TestMessageTool_Execute_WithCustomChannel(t *testing.T) {
})
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"content": "Test message",
"channel": "custom-channel",
"chat_id": "custom-chat-id",
@@ -104,7 +104,7 @@ func TestMessageTool_Execute_SendFailure(t *testing.T) {
})
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"content": "Test message",
}
@@ -136,7 +136,7 @@ func TestMessageTool_Execute_MissingContent(t *testing.T) {
tool.SetContext("test-channel", "test-chat-id")
ctx := context.Background()
- args := map[string]interface{}{} // content missing
+ args := map[string]any{} // content missing
result := tool.Execute(ctx, args)
@@ -158,7 +158,7 @@ func TestMessageTool_Execute_NoTargetChannel(t *testing.T) {
})
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"content": "Test message",
}
@@ -179,7 +179,7 @@ func TestMessageTool_Execute_NotConfigured(t *testing.T) {
// No SetSendCallback called
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"content": "Test message",
}
@@ -219,7 +219,7 @@ func TestMessageTool_Parameters(t *testing.T) {
t.Error("Expected type 'object'")
}
- props, ok := params["properties"].(map[string]interface{})
+ props, ok := params["properties"].(map[string]any)
if !ok {
t.Fatal("Expected properties to be a map")
}
@@ -231,7 +231,7 @@ func TestMessageTool_Parameters(t *testing.T) {
}
// Check content property
- contentProp, ok := props["content"].(map[string]interface{})
+ contentProp, ok := props["content"].(map[string]any)
if !ok {
t.Error("Expected 'content' property")
}
@@ -240,7 +240,7 @@ func TestMessageTool_Parameters(t *testing.T) {
}
// Check channel property (optional)
- channelProp, ok := props["channel"].(map[string]interface{})
+ channelProp, ok := props["channel"].(map[string]any)
if !ok {
t.Error("Expected 'channel' property")
}
@@ -249,7 +249,7 @@ func TestMessageTool_Parameters(t *testing.T) {
}
// Check chat_id property (optional)
- chatIDProp, ok := props["chat_id"].(map[string]interface{})
+ chatIDProp, ok := props["chat_id"].(map[string]any)
if !ok {
t.Error("Expected 'chat_id' property")
}
diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go
index c8cf92863..6ecb8ae7c 100644
--- a/pkg/tools/registry.go
+++ b/pkg/tools/registry.go
@@ -34,16 +34,22 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) {
return tool, ok
}
-func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]interface{}) *ToolResult {
+func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]any) *ToolResult {
return r.ExecuteWithContext(ctx, name, args, "", "", nil)
}
// ExecuteWithContext executes a tool with channel/chatID context and optional async callback.
// If the tool implements AsyncTool and a non-nil callback is provided,
// the callback will be set on the tool before execution.
-func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args map[string]interface{}, channel, chatID string, asyncCallback AsyncCallback) *ToolResult {
+func (r *ToolRegistry) ExecuteWithContext(
+ ctx context.Context,
+ name string,
+ args map[string]any,
+ channel, chatID string,
+ asyncCallback AsyncCallback,
+) *ToolResult {
logger.InfoCF("tool", "Tool execution started",
- map[string]interface{}{
+ map[string]any{
"tool": name,
"args": args,
})
@@ -51,7 +57,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
tool, ok := r.Get(name)
if !ok {
logger.ErrorCF("tool", "Tool not found",
- map[string]interface{}{
+ map[string]any{
"tool": name,
})
return ErrorResult(fmt.Sprintf("tool %q not found", name)).WithError(fmt.Errorf("tool not found"))
@@ -66,7 +72,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
if asyncTool, ok := tool.(AsyncTool); ok && asyncCallback != nil {
asyncTool.SetCallback(asyncCallback)
logger.DebugCF("tool", "Async callback injected",
- map[string]interface{}{
+ map[string]any{
"tool": name,
})
}
@@ -78,20 +84,20 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
// Log based on result type
if result.IsError {
logger.ErrorCF("tool", "Tool execution failed",
- map[string]interface{}{
+ map[string]any{
"tool": name,
"duration": duration.Milliseconds(),
"error": result.ForLLM,
})
} else if result.Async {
logger.InfoCF("tool", "Tool started (async)",
- map[string]interface{}{
+ map[string]any{
"tool": name,
"duration": duration.Milliseconds(),
})
} else {
logger.InfoCF("tool", "Tool execution completed",
- map[string]interface{}{
+ map[string]any{
"tool": name,
"duration_ms": duration.Milliseconds(),
"result_length": len(result.ForLLM),
@@ -101,11 +107,11 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
return result
}
-func (r *ToolRegistry) GetDefinitions() []map[string]interface{} {
+func (r *ToolRegistry) GetDefinitions() []map[string]any {
r.mu.RLock()
defer r.mu.RUnlock()
- definitions := make([]map[string]interface{}, 0, len(r.tools))
+ definitions := make([]map[string]any, 0, len(r.tools))
for _, tool := range r.tools {
definitions = append(definitions, ToolToSchema(tool))
}
@@ -123,14 +129,14 @@ func (r *ToolRegistry) ToProviderDefs() []providers.ToolDefinition {
schema := ToolToSchema(tool)
// Safely extract nested values with type checks
- fn, ok := schema["function"].(map[string]interface{})
+ fn, ok := schema["function"].(map[string]any)
if !ok {
continue
}
name, _ := fn["name"].(string)
desc, _ := fn["description"].(string)
- params, _ := fn["parameters"].(map[string]interface{})
+ params, _ := fn["parameters"].(map[string]any)
definitions = append(definitions, providers.ToolDefinition{
Type: "function",
diff --git a/pkg/tools/result_test.go b/pkg/tools/result_test.go
index bc798cd70..a234e33f3 100644
--- a/pkg/tools/result_test.go
+++ b/pkg/tools/result_test.go
@@ -192,7 +192,7 @@ func TestToolResultJSONStructure(t *testing.T) {
}
// Verify JSON structure
- var parsed map[string]interface{}
+ var parsed map[string]any
if err := json.Unmarshal(data, &parsed); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go
index d9430672f..a1ee0b6e1 100644
--- a/pkg/tools/shell.go
+++ b/pkg/tools/shell.go
@@ -3,6 +3,7 @@ package tools
import (
"bytes"
"context"
+ "errors"
"fmt"
"os"
"os/exec"
@@ -118,15 +119,15 @@ func (t *ExecTool) Description() string {
return "Execute a shell command and return its output. Use with caution."
}
-func (t *ExecTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *ExecTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "command": map[string]interface{}{
+ "properties": map[string]any{
+ "command": map[string]any{
"type": "string",
"description": "The shell command to execute",
},
- "working_dir": map[string]interface{}{
+ "working_dir": map[string]any{
"type": "string",
"description": "Optional working directory for the command",
},
@@ -135,7 +136,7 @@ func (t *ExecTool) Parameters() map[string]interface{} {
}
}
-func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
command, ok := args["command"].(string)
if !ok {
return ErrorResult("command is required")
@@ -143,7 +144,15 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *To
cwd := t.workingDir
if wd, ok := args["working_dir"].(string); ok && wd != "" {
- cwd = wd
+ if t.restrictToWorkspace && t.workingDir != "" {
+ resolvedWD, err := validatePath(wd, t.workingDir, true)
+ if err != nil {
+ return ErrorResult("Command blocked by safety guard (" + err.Error() + ")")
+ }
+ cwd = resolvedWD
+ } else {
+ cwd = wd
+ }
}
if cwd == "" {
@@ -177,18 +186,43 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *To
cmd.Dir = cwd
}
+ prepareCommandForTermination(cmd)
+
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
- err := cmd.Run()
+ if err := cmd.Start(); err != nil {
+ return ErrorResult(fmt.Sprintf("failed to start command: %v", err))
+ }
+
+ done := make(chan error, 1)
+ go func() {
+ done <- cmd.Wait()
+ }()
+
+ var err error
+ select {
+ case err = <-done:
+ case <-cmdCtx.Done():
+ _ = terminateProcessTree(cmd)
+ select {
+ case err = <-done:
+ case <-time.After(2 * time.Second):
+ if cmd.Process != nil {
+ _ = cmd.Process.Kill()
+ }
+ err = <-done
+ }
+ }
+
output := stdout.String()
if stderr.Len() > 0 {
output += "\nSTDERR:\n" + stderr.String()
}
if err != nil {
- if cmdCtx.Err() == context.DeadlineExceeded {
+ if errors.Is(cmdCtx.Err(), context.DeadlineExceeded) {
msg := fmt.Sprintf("Command timed out after %v", t.timeout)
return &ToolResult{
ForLLM: msg,
diff --git a/pkg/tools/shell_process_unix.go b/pkg/tools/shell_process_unix.go
new file mode 100644
index 000000000..7b29a81bf
--- /dev/null
+++ b/pkg/tools/shell_process_unix.go
@@ -0,0 +1,32 @@
+//go:build !windows
+
+package tools
+
+import (
+ "os/exec"
+ "syscall"
+)
+
+func prepareCommandForTermination(cmd *exec.Cmd) {
+ if cmd == nil {
+ return
+ }
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+}
+
+func terminateProcessTree(cmd *exec.Cmd) error {
+ if cmd == nil || cmd.Process == nil {
+ return nil
+ }
+
+ pid := cmd.Process.Pid
+ if pid <= 0 {
+ return nil
+ }
+
+ // Kill the entire process group spawned by the shell command.
+ _ = syscall.Kill(-pid, syscall.SIGKILL)
+ // Fallback kill on the shell process itself.
+ _ = cmd.Process.Kill()
+ return nil
+}
diff --git a/pkg/tools/shell_process_windows.go b/pkg/tools/shell_process_windows.go
new file mode 100644
index 000000000..fe23b5c96
--- /dev/null
+++ b/pkg/tools/shell_process_windows.go
@@ -0,0 +1,27 @@
+//go:build windows
+
+package tools
+
+import (
+ "os/exec"
+ "strconv"
+)
+
+func prepareCommandForTermination(cmd *exec.Cmd) {
+ // no-op on Windows
+}
+
+func terminateProcessTree(cmd *exec.Cmd) error {
+ if cmd == nil || cmd.Process == nil {
+ return nil
+ }
+
+ pid := cmd.Process.Pid
+ if pid <= 0 {
+ return nil
+ }
+
+ _ = exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(pid)).Run()
+ _ = cmd.Process.Kill()
+ return nil
+}
diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go
index c06468a39..6d35815e8 100644
--- a/pkg/tools/shell_test.go
+++ b/pkg/tools/shell_test.go
@@ -14,7 +14,7 @@ func TestShellTool_Success(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"command": "echo 'hello world'",
}
@@ -41,7 +41,7 @@ func TestShellTool_Failure(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"command": "ls /nonexistent_directory_12345",
}
@@ -69,7 +69,7 @@ func TestShellTool_Timeout(t *testing.T) {
tool.SetTimeout(100 * time.Millisecond)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"command": "sleep 10",
}
@@ -91,12 +91,12 @@ func TestShellTool_WorkingDir(t *testing.T) {
// Create temp directory
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
- os.WriteFile(testFile, []byte("test content"), 0644)
+ os.WriteFile(testFile, []byte("test content"), 0o644)
tool := NewExecTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"command": "cat test.txt",
"working_dir": tmpDir,
}
@@ -117,7 +117,7 @@ func TestShellTool_DangerousCommand(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"command": "rm -rf /",
}
@@ -138,7 +138,7 @@ func TestShellTool_MissingCommand(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
- args := map[string]interface{}{}
+ args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -153,7 +153,7 @@ func TestShellTool_StderrCapture(t *testing.T) {
tool := NewExecTool("", false)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"command": "sh -c 'echo stdout; echo stderr >&2'",
}
@@ -174,7 +174,7 @@ func TestShellTool_OutputTruncation(t *testing.T) {
ctx := context.Background()
// Generate long output (>10000 chars)
- args := map[string]interface{}{
+ args := map[string]any{
"command": "python3 -c \"print('x' * 20000)\" || echo " + strings.Repeat("x", 20000),
}
@@ -186,6 +186,66 @@ func TestShellTool_OutputTruncation(t *testing.T) {
}
}
+// TestShellTool_WorkingDir_OutsideWorkspace verifies that working_dir cannot escape the workspace directly
+func TestShellTool_WorkingDir_OutsideWorkspace(t *testing.T) {
+ root := t.TempDir()
+ workspace := filepath.Join(root, "workspace")
+ outsideDir := filepath.Join(root, "outside")
+ if err := os.MkdirAll(workspace, 0o755); err != nil {
+ t.Fatalf("failed to create workspace: %v", err)
+ }
+ if err := os.MkdirAll(outsideDir, 0o755); err != nil {
+ t.Fatalf("failed to create outside dir: %v", err)
+ }
+
+ tool := NewExecTool(workspace, true)
+ result := tool.Execute(context.Background(), map[string]any{
+ "command": "pwd",
+ "working_dir": outsideDir,
+ })
+
+ if !result.IsError {
+ t.Fatalf("expected working_dir outside workspace to be blocked, got output: %s", result.ForLLM)
+ }
+ if !strings.Contains(result.ForLLM, "blocked") {
+ t.Errorf("expected 'blocked' in error, got: %s", result.ForLLM)
+ }
+}
+
+// TestShellTool_WorkingDir_SymlinkEscape verifies that a symlink inside the workspace
+// pointing outside cannot be used as working_dir to escape the sandbox.
+func TestShellTool_WorkingDir_SymlinkEscape(t *testing.T) {
+ root := t.TempDir()
+ workspace := filepath.Join(root, "workspace")
+ secretDir := filepath.Join(root, "secret")
+ if err := os.MkdirAll(workspace, 0o755); err != nil {
+ t.Fatalf("failed to create workspace: %v", err)
+ }
+ if err := os.MkdirAll(secretDir, 0o755); err != nil {
+ t.Fatalf("failed to create secret dir: %v", err)
+ }
+ os.WriteFile(filepath.Join(secretDir, "secret.txt"), []byte("top secret"), 0o644)
+
+ // symlink lives inside the workspace but resolves to secretDir outside it
+ link := filepath.Join(workspace, "escape")
+ if err := os.Symlink(secretDir, link); err != nil {
+ t.Skipf("symlinks not supported in this environment: %v", err)
+ }
+
+ tool := NewExecTool(workspace, true)
+ result := tool.Execute(context.Background(), map[string]any{
+ "command": "cat secret.txt",
+ "working_dir": link,
+ })
+
+ if !result.IsError {
+ t.Fatalf("expected symlink working_dir escape to be blocked, got output: %s", result.ForLLM)
+ }
+ if !strings.Contains(result.ForLLM, "blocked") {
+ t.Errorf("expected 'blocked' in error, got: %s", result.ForLLM)
+ }
+}
+
// TestShellTool_RestrictToWorkspace verifies workspace restriction
func TestShellTool_RestrictToWorkspace(t *testing.T) {
tmpDir := t.TempDir()
@@ -193,7 +253,7 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
tool.SetRestrictToWorkspace(true)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"command": "cat ../../etc/passwd",
}
@@ -205,6 +265,10 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
}
if !strings.Contains(result.ForLLM, "blocked") && !strings.Contains(result.ForUser, "blocked") {
- t.Errorf("Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s", result.ForLLM, result.ForUser)
+ t.Errorf(
+ "Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s",
+ result.ForLLM,
+ result.ForUser,
+ )
}
}
diff --git a/pkg/tools/shell_timeout_unix_test.go b/pkg/tools/shell_timeout_unix_test.go
new file mode 100644
index 000000000..04ef8e441
--- /dev/null
+++ b/pkg/tools/shell_timeout_unix_test.go
@@ -0,0 +1,61 @@
+//go:build !windows
+
+package tools
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+)
+
+func processExists(pid int) bool {
+ if pid <= 0 {
+ return false
+ }
+ err := syscall.Kill(pid, 0)
+ return err == nil || err == syscall.EPERM
+}
+
+func TestShellTool_TimeoutKillsChildProcess(t *testing.T) {
+ tool := NewExecTool(t.TempDir(), false)
+ tool.SetTimeout(500 * time.Millisecond)
+
+ args := map[string]any{
+ // Spawn a child process that would outlive the shell unless process-group kill is used.
+ "command": "sleep 60 & echo $! > child.pid; wait",
+ }
+
+ result := tool.Execute(context.Background(), args)
+ if !result.IsError {
+ t.Fatalf("expected timeout error, got success: %s", result.ForLLM)
+ }
+ if !strings.Contains(result.ForLLM, "timed out") {
+ t.Fatalf("expected timeout message, got: %s", result.ForLLM)
+ }
+
+ childPIDPath := filepath.Join(tool.workingDir, "child.pid")
+ data, err := os.ReadFile(childPIDPath)
+ if err != nil {
+ t.Fatalf("failed to read child pid file: %v", err)
+ }
+
+ childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
+ if err != nil {
+ t.Fatalf("failed to parse child pid: %v", err)
+ }
+
+ deadline := time.Now().Add(2 * time.Second)
+ for time.Now().Before(deadline) {
+ if !processExists(childPID) {
+ return
+ }
+ time.Sleep(50 * time.Millisecond)
+ }
+
+ t.Fatalf("child process %d is still running after timeout", childPID)
+}
diff --git a/pkg/tools/skills_install.go b/pkg/tools/skills_install.go
index 6b05918ce..55c0b678d 100644
--- a/pkg/tools/skills_install.go
+++ b/pkg/tools/skills_install.go
@@ -42,23 +42,23 @@ func (t *InstallSkillTool) Description() string {
return "Install a skill from a registry by slug. Downloads and extracts the skill into the workspace. Use find_skills first to discover available skills."
}
-func (t *InstallSkillTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *InstallSkillTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "slug": map[string]interface{}{
+ "properties": map[string]any{
+ "slug": map[string]any{
"type": "string",
"description": "The unique slug of the skill to install (e.g., 'github', 'docker-compose')",
},
- "version": map[string]interface{}{
+ "version": map[string]any{
"type": "string",
"description": "Specific version to install (optional, defaults to latest)",
},
- "registry": map[string]interface{}{
+ "registry": map[string]any{
"type": "string",
"description": "Registry to install from (required, e.g., 'clawhub')",
},
- "force": map[string]interface{}{
+ "force": map[string]any{
"type": "boolean",
"description": "Force reinstall if skill already exists (default false)",
},
@@ -67,7 +67,7 @@ func (t *InstallSkillTool) Parameters() map[string]interface{} {
}
}
-func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
// Install lock to prevent concurrent directory operations.
// Ideally this should be done at a `slug` level, currently, its at a `workspace` level.
t.mu.Lock()
@@ -94,7 +94,9 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
if !force {
if _, err := os.Stat(targetDir); err == nil {
- return ErrorResult(fmt.Sprintf("skill %q already installed at %s. Use force=true to reinstall.", slug, targetDir))
+ return ErrorResult(
+ fmt.Sprintf("skill %q already installed at %s. Use force=true to reinstall.", slug, targetDir),
+ )
}
} else {
// Force: remove existing if present.
@@ -108,7 +110,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
}
// Ensure skills directory exists.
- if err := os.MkdirAll(skillsDir, 0755); err != nil {
+ if err := os.MkdirAll(skillsDir, 0o755); err != nil {
return ErrorResult(fmt.Sprintf("failed to create skills directory: %v", err))
}
@@ -119,7 +121,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
rmErr := os.RemoveAll(targetDir)
if rmErr != nil {
logger.ErrorCF("tool", "Failed to remove partial install",
- map[string]interface{}{
+ map[string]any{
"tool": "install_skill",
"target_dir": targetDir,
"error": rmErr.Error(),
@@ -133,7 +135,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
rmErr := os.RemoveAll(targetDir)
if rmErr != nil {
logger.ErrorCF("tool", "Failed to remove partial install",
- map[string]interface{}{
+ map[string]any{
"tool": "install_skill",
"target_dir": targetDir,
"error": rmErr.Error(),
@@ -145,7 +147,7 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac
// Write origin metadata.
if err := writeOriginMeta(targetDir, registry.Name(), slug, result.Version); err != nil {
logger.ErrorCF("tool", "Failed to write origin metadata",
- map[string]interface{}{
+ map[string]any{
"tool": "install_skill",
"error": err.Error(),
"target": targetDir,
@@ -195,5 +197,5 @@ func writeOriginMeta(targetDir, registryName, slug, version string) error {
return err
}
- return os.WriteFile(filepath.Join(targetDir, ".skill-origin.json"), data, 0644)
+ return os.WriteFile(filepath.Join(targetDir, ".skill-origin.json"), data, 0o644)
}
diff --git a/pkg/tools/skills_install_test.go b/pkg/tools/skills_install_test.go
index e6941a950..676fcecc0 100644
--- a/pkg/tools/skills_install_test.go
+++ b/pkg/tools/skills_install_test.go
@@ -6,9 +6,10 @@ import (
"path/filepath"
"testing"
- "github.com/sipeed/picoclaw/pkg/skills"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/sipeed/picoclaw/pkg/skills"
)
func TestInstallSkillToolName(t *testing.T) {
@@ -18,14 +19,14 @@ func TestInstallSkillToolName(t *testing.T) {
func TestInstallSkillToolMissingSlug(t *testing.T) {
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
- result := tool.Execute(context.Background(), map[string]interface{}{})
+ result := tool.Execute(context.Background(), map[string]any{})
assert.True(t, result.IsError)
assert.Contains(t, result.ForLLM, "identifier is required and must be a non-empty string")
}
func TestInstallSkillToolEmptySlug(t *testing.T) {
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"slug": " ",
})
assert.True(t, result.IsError)
@@ -42,7 +43,7 @@ func TestInstallSkillToolUnsafeSlug(t *testing.T) {
}
for _, slug := range cases {
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"slug": slug,
})
assert.True(t, result.IsError, "slug %q should be rejected", slug)
@@ -53,10 +54,10 @@ func TestInstallSkillToolUnsafeSlug(t *testing.T) {
func TestInstallSkillToolAlreadyExists(t *testing.T) {
workspace := t.TempDir()
skillDir := filepath.Join(workspace, "skills", "existing-skill")
- require.NoError(t, os.MkdirAll(skillDir, 0755))
+ require.NoError(t, os.MkdirAll(skillDir, 0o755))
tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace)
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"slug": "existing-skill",
"registry": "clawhub",
})
@@ -67,7 +68,7 @@ func TestInstallSkillToolAlreadyExists(t *testing.T) {
func TestInstallSkillToolRegistryNotFound(t *testing.T) {
workspace := t.TempDir()
tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace)
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"slug": "some-skill",
"registry": "nonexistent",
})
@@ -80,7 +81,7 @@ func TestInstallSkillToolParameters(t *testing.T) {
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
params := tool.Parameters()
- props, ok := params["properties"].(map[string]interface{})
+ props, ok := params["properties"].(map[string]any)
assert.True(t, ok)
assert.Contains(t, props, "slug")
assert.Contains(t, props, "version")
@@ -95,7 +96,7 @@ func TestInstallSkillToolParameters(t *testing.T) {
func TestInstallSkillToolMissingRegistry(t *testing.T) {
tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir())
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"slug": "some-skill",
})
assert.True(t, result.IsError)
diff --git a/pkg/tools/skills_search.go b/pkg/tools/skills_search.go
index b12949ec2..2b6cffd38 100644
--- a/pkg/tools/skills_search.go
+++ b/pkg/tools/skills_search.go
@@ -32,15 +32,15 @@ func (t *FindSkillsTool) Description() string {
return "Search for installable skills from skill registries. Returns skill slugs, descriptions, versions, and relevance scores. Use this to discover skills before installing them with install_skill."
}
-func (t *FindSkillsTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *FindSkillsTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
+ "properties": map[string]any{
+ "query": map[string]any{
"type": "string",
"description": "Search query describing the desired skill capability (e.g., 'github integration', 'database management')",
},
- "limit": map[string]interface{}{
+ "limit": map[string]any{
"type": "integer",
"description": "Maximum number of results to return (1-20, default 5)",
"minimum": 1.0,
@@ -51,7 +51,7 @@ func (t *FindSkillsTool) Parameters() map[string]interface{} {
}
}
-func (t *FindSkillsTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *FindSkillsTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
query, ok := args["query"].(string)
query = strings.ToLower(strings.TrimSpace(query))
if !ok || query == "" {
diff --git a/pkg/tools/skills_search_test.go b/pkg/tools/skills_search_test.go
index 7e07b2775..0e5387cf5 100644
--- a/pkg/tools/skills_search_test.go
+++ b/pkg/tools/skills_search_test.go
@@ -4,8 +4,9 @@ import (
"context"
"testing"
- "github.com/sipeed/picoclaw/pkg/skills"
"github.com/stretchr/testify/assert"
+
+ "github.com/sipeed/picoclaw/pkg/skills"
)
func TestFindSkillsToolName(t *testing.T) {
@@ -15,14 +16,14 @@ func TestFindSkillsToolName(t *testing.T) {
func TestFindSkillsToolMissingQuery(t *testing.T) {
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
- result := tool.Execute(context.Background(), map[string]interface{}{})
+ result := tool.Execute(context.Background(), map[string]any{})
assert.True(t, result.IsError)
assert.Contains(t, result.ForLLM, "query is required")
}
func TestFindSkillsToolEmptyQuery(t *testing.T) {
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"query": " ",
})
assert.True(t, result.IsError)
@@ -35,7 +36,7 @@ func TestFindSkillsToolCacheHit(t *testing.T) {
})
tool := NewFindSkillsTool(skills.NewRegistryManager(), cache)
- result := tool.Execute(context.Background(), map[string]interface{}{
+ result := tool.Execute(context.Background(), map[string]any{
"query": "github",
})
@@ -48,7 +49,7 @@ func TestFindSkillsToolParameters(t *testing.T) {
tool := NewFindSkillsTool(skills.NewRegistryManager(), nil)
params := tool.Parameters()
- props, ok := params["properties"].(map[string]interface{})
+ props, ok := params["properties"].(map[string]any)
assert.True(t, ok)
assert.Contains(t, props, "query")
assert.Contains(t, props, "limit")
@@ -71,7 +72,14 @@ func TestFormatSearchResultsEmpty(t *testing.T) {
func TestFormatSearchResultsWithData(t *testing.T) {
results := []skills.SearchResult{
- {Slug: "github", Score: 0.95, DisplayName: "GitHub", Summary: "GitHub API integration", Version: "1.0.0", RegistryName: "clawhub"},
+ {
+ Slug: "github",
+ Score: 0.95,
+ DisplayName: "GitHub",
+ Summary: "GitHub API integration",
+ Version: "1.0.0",
+ RegistryName: "clawhub",
+ },
}
output := formatSearchResults("github", results, false)
assert.Contains(t, output, "github")
diff --git a/pkg/tools/spawn.go b/pkg/tools/spawn.go
index f01372467..73d385cb0 100644
--- a/pkg/tools/spawn.go
+++ b/pkg/tools/spawn.go
@@ -34,19 +34,19 @@ func (t *SpawnTool) Description() string {
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done."
}
-func (t *SpawnTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *SpawnTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "task": map[string]interface{}{
+ "properties": map[string]any{
+ "task": map[string]any{
"type": "string",
"description": "The task for subagent to complete",
},
- "label": map[string]interface{}{
+ "label": map[string]any{
"type": "string",
"description": "Optional short label for the task (for display)",
},
- "agent_id": map[string]interface{}{
+ "agent_id": map[string]any{
"type": "string",
"description": "Optional target agent ID to delegate the task to",
},
@@ -64,7 +64,7 @@ func (t *SpawnTool) SetAllowlistChecker(check func(targetAgentID string) bool) {
t.allowlistCheck = check
}
-func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
task, ok := args["task"].(string)
if !ok {
return ErrorResult("task is required")
diff --git a/pkg/tools/spi.go b/pkg/tools/spi.go
index 4805d6a35..d6a88a5b0 100644
--- a/pkg/tools/spi.go
+++ b/pkg/tools/spi.go
@@ -24,41 +24,41 @@ func (t *SPITool) Description() string {
return "Interact with SPI bus devices for high-speed peripheral communication. Actions: list (find SPI devices), transfer (full-duplex send/receive), read (receive bytes). Linux only."
}
-func (t *SPITool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *SPITool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "action": map[string]interface{}{
+ "properties": map[string]any{
+ "action": map[string]any{
"type": "string",
"enum": []string{"list", "transfer", "read"},
"description": "Action to perform: list (find available SPI devices), transfer (full-duplex send/receive), read (receive bytes by sending zeros)",
},
- "device": map[string]interface{}{
+ "device": map[string]any{
"type": "string",
"description": "SPI device identifier (e.g. \"2.0\" for /dev/spidev2.0). Required for transfer/read.",
},
- "speed": map[string]interface{}{
+ "speed": map[string]any{
"type": "integer",
"description": "SPI clock speed in Hz. Default: 1000000 (1 MHz).",
},
- "mode": map[string]interface{}{
+ "mode": map[string]any{
"type": "integer",
"description": "SPI mode (0-3). Default: 0. Mode sets CPOL and CPHA: 0=0,0 1=0,1 2=1,0 3=1,1.",
},
- "bits": map[string]interface{}{
+ "bits": map[string]any{
"type": "integer",
"description": "Bits per word. Default: 8.",
},
- "data": map[string]interface{}{
+ "data": map[string]any{
"type": "array",
- "items": map[string]interface{}{"type": "integer"},
+ "items": map[string]any{"type": "integer"},
"description": "Bytes to send (0-255 each). Required for transfer action.",
},
- "length": map[string]interface{}{
+ "length": map[string]any{
"type": "integer",
"description": "Number of bytes to read (1-4096). Required for read action.",
},
- "confirm": map[string]interface{}{
+ "confirm": map[string]any{
"type": "boolean",
"description": "Must be true for transfer operations. Safety guard to prevent accidental writes.",
},
@@ -67,7 +67,7 @@ func (t *SPITool) Parameters() map[string]interface{} {
}
}
-func (t *SPITool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *SPITool) Execute(ctx context.Context, args map[string]any) *ToolResult {
if runtime.GOOS != "linux" {
return ErrorResult("SPI is only supported on Linux. This tool requires /dev/spidev* device files.")
}
@@ -97,7 +97,9 @@ func (t *SPITool) list() *ToolResult {
}
if len(matches) == 0 {
- return SilentResult("No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded")
+ return SilentResult(
+ "No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded",
+ )
}
type devInfo struct {
@@ -118,7 +120,7 @@ func (t *SPITool) list() *ToolResult {
}
// parseSPIArgs extracts and validates common SPI parameters
-func parseSPIArgs(args map[string]interface{}) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
+func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
dev, ok := args["device"].(string)
if !ok || dev == "" {
return "", 0, 0, 0, "device is required (e.g. \"2.0\" for /dev/spidev2.0)"
diff --git a/pkg/tools/spi_linux.go b/pkg/tools/spi_linux.go
index 12b696007..9def73662 100644
--- a/pkg/tools/spi_linux.go
+++ b/pkg/tools/spi_linux.go
@@ -66,10 +66,12 @@ func configureSPI(devPath string, mode uint8, bits uint8, speed uint32) (int, *T
}
// transfer performs a full-duplex SPI transfer
-func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
+func (t *SPITool) transfer(args map[string]any) *ToolResult {
confirm, _ := args["confirm"].(bool)
if !confirm {
- return ErrorResult("transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.")
+ return ErrorResult(
+ "transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.",
+ )
}
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
@@ -77,7 +79,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
return ErrorResult(errMsg)
}
- dataRaw, ok := args["data"].([]interface{})
+ dataRaw, ok := args["data"].([]any)
if !ok || len(dataRaw) == 0 {
return ErrorResult("data is required for transfer (array of byte values 0-255)")
}
@@ -130,7 +132,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
intBytes[i] = int(b)
}
- result, _ := json.MarshalIndent(map[string]interface{}{
+ result, _ := json.MarshalIndent(map[string]any{
"device": devPath,
"sent": len(txBuf),
"received": intBytes,
@@ -140,7 +142,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
}
// readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed)
-func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
+func (t *SPITool) readDevice(args map[string]any) *ToolResult {
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
if errMsg != "" {
return ErrorResult(errMsg)
@@ -186,7 +188,7 @@ func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
intBytes[i] = int(b)
}
- result, _ := json.MarshalIndent(map[string]interface{}{
+ result, _ := json.MarshalIndent(map[string]any{
"device": devPath,
"bytes": intBytes,
"hex": hexBytes,
diff --git a/pkg/tools/spi_other.go b/pkg/tools/spi_other.go
index 6dfc86fd1..5d078ac3f 100644
--- a/pkg/tools/spi_other.go
+++ b/pkg/tools/spi_other.go
@@ -3,11 +3,11 @@
package tools
// transfer is a stub for non-Linux platforms.
-func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
+func (t *SPITool) transfer(args map[string]any) *ToolResult {
return ErrorResult("SPI is only supported on Linux")
}
// readDevice is a stub for non-Linux platforms.
-func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
+func (t *SPITool) readDevice(args map[string]any) *ToolResult {
return ErrorResult("SPI is only supported on Linux")
}
diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go
index 294ba6ea8..91ebff636 100644
--- a/pkg/tools/subagent.go
+++ b/pkg/tools/subagent.go
@@ -38,7 +38,11 @@ type SubagentManager struct {
nextID int
}
-func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager {
+func NewSubagentManager(
+ provider providers.LLMProvider,
+ defaultModel, workspace string,
+ bus *bus.MessageBus,
+) *SubagentManager {
return &SubagentManager{
tasks: make(map[string]*SubagentTask),
provider: provider,
@@ -76,7 +80,11 @@ func (sm *SubagentManager) RegisterTool(tool Tool) {
sm.tools.Register(tool)
}
-func (sm *SubagentManager) Spawn(ctx context.Context, task, label, agentID, originChannel, originChatID string, callback AsyncCallback) (string, error) {
+func (sm *SubagentManager) Spawn(
+ ctx context.Context,
+ task, label, agentID, originChannel, originChatID string,
+ callback AsyncCallback,
+) (string, error) {
sm.mu.Lock()
defer sm.mu.Unlock()
@@ -194,7 +202,12 @@ After completing the task, provide a clear summary of what was done.`
task.Status = "completed"
task.Result = loopResult.Content
result = &ToolResult{
- ForLLM: fmt.Sprintf("Subagent '%s' completed (iterations: %d): %s", task.Label, loopResult.Iterations, loopResult.Content),
+ ForLLM: fmt.Sprintf(
+ "Subagent '%s' completed (iterations: %d): %s",
+ task.Label,
+ loopResult.Iterations,
+ loopResult.Content,
+ ),
ForUser: loopResult.Content,
Silent: false,
IsError: false,
@@ -258,15 +271,15 @@ func (t *SubagentTool) Description() string {
return "Execute a subagent task synchronously and return the result. Use this for delegating specific tasks to an independent agent instance. Returns execution summary to user and full details to LLM."
}
-func (t *SubagentTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *SubagentTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "task": map[string]interface{}{
+ "properties": map[string]any{
+ "task": map[string]any{
"type": "string",
"description": "The task for subagent to complete",
},
- "label": map[string]interface{}{
+ "label": map[string]any{
"type": "string",
"description": "Optional short label for the task (for display)",
},
@@ -280,7 +293,7 @@ func (t *SubagentTool) SetContext(channel, chatID string) {
t.originChatID = chatID
}
-func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
task, ok := args["task"].(string)
if !ok {
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
diff --git a/pkg/tools/subagent_tool_test.go b/pkg/tools/subagent_tool_test.go
index f960a7fda..59bfdffae 100644
--- a/pkg/tools/subagent_tool_test.go
+++ b/pkg/tools/subagent_tool_test.go
@@ -11,10 +11,16 @@ import (
// MockLLMProvider is a test implementation of LLMProvider
type MockLLMProvider struct {
- lastOptions map[string]interface{}
+ lastOptions map[string]any
}
-func (m *MockLLMProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
+func (m *MockLLMProvider) Chat(
+ ctx context.Context,
+ messages []providers.Message,
+ tools []providers.ToolDefinition,
+ model string,
+ options map[string]any,
+) (*providers.LLMResponse, error) {
m.lastOptions = options
// Find the last user message to generate a response
for i := len(messages) - 1; i >= 0; i-- {
@@ -47,7 +53,7 @@ func TestSubagentManager_SetLLMOptions_AppliesToRunToolLoop(t *testing.T) {
tool.SetContext("cli", "direct")
ctx := context.Background()
- args := map[string]interface{}{"task": "Do something"}
+ args := map[string]any{"task": "Do something"}
result := tool.Execute(ctx, args)
if result == nil || result.IsError {
@@ -108,13 +114,13 @@ func TestSubagentTool_Parameters(t *testing.T) {
}
// Check properties
- props, ok := params["properties"].(map[string]interface{})
+ props, ok := params["properties"].(map[string]any)
if !ok {
t.Fatal("Properties should be a map")
}
// Verify task parameter
- task, ok := props["task"].(map[string]interface{})
+ task, ok := props["task"].(map[string]any)
if !ok {
t.Fatal("Task parameter should exist")
}
@@ -123,7 +129,7 @@ func TestSubagentTool_Parameters(t *testing.T) {
}
// Verify label parameter
- label, ok := props["label"].(map[string]interface{})
+ label, ok := props["label"].(map[string]any)
if !ok {
t.Fatal("Label parameter should exist")
}
@@ -163,7 +169,7 @@ func TestSubagentTool_Execute_Success(t *testing.T) {
tool.SetContext("telegram", "chat-123")
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"task": "Write a haiku about coding",
"label": "haiku-task",
}
@@ -218,7 +224,7 @@ func TestSubagentTool_Execute_NoLabel(t *testing.T) {
tool := NewSubagentTool(manager)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"task": "Test task without label",
}
@@ -241,7 +247,7 @@ func TestSubagentTool_Execute_MissingTask(t *testing.T) {
tool := NewSubagentTool(manager)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"label": "test",
}
@@ -268,7 +274,7 @@ func TestSubagentTool_Execute_NilManager(t *testing.T) {
tool := NewSubagentTool(nil)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"task": "test task",
}
@@ -297,7 +303,7 @@ func TestSubagentTool_Execute_ContextPassing(t *testing.T) {
tool.SetContext(channel, chatID)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"task": "Test context passing",
}
@@ -324,7 +330,7 @@ func TestSubagentTool_ForUserTruncation(t *testing.T) {
// Create a task that will generate long response
longTask := strings.Repeat("This is a very long task description. ", 100)
- args := map[string]interface{}{
+ args := map[string]any{
"task": longTask,
"label": "long-test",
}
diff --git a/pkg/tools/toolloop.go b/pkg/tools/toolloop.go
index 08f14cc92..cdfe0d6ce 100644
--- a/pkg/tools/toolloop.go
+++ b/pkg/tools/toolloop.go
@@ -33,7 +33,12 @@ type ToolLoopResult struct {
// RunToolLoop executes the LLM + tool call iteration loop.
// This is the core agent logic that can be reused by both main agent and subagents.
-func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []providers.Message, channel, chatID string) (*ToolLoopResult, error) {
+func RunToolLoop(
+ ctx context.Context,
+ config ToolLoopConfig,
+ messages []providers.Message,
+ channel, chatID string,
+) (*ToolLoopResult, error) {
iteration := 0
var finalContent string
diff --git a/pkg/tools/types.go b/pkg/tools/types.go
index f8205b8bd..a6015cde3 100644
--- a/pkg/tools/types.go
+++ b/pkg/tools/types.go
@@ -10,11 +10,11 @@ type Message struct {
}
type ToolCall struct {
- ID string `json:"id"`
- Type string `json:"type"`
- Function *FunctionCall `json:"function,omitempty"`
- Name string `json:"name,omitempty"`
- Arguments map[string]interface{} `json:"arguments,omitempty"`
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Function *FunctionCall `json:"function,omitempty"`
+ Name string `json:"name,omitempty"`
+ Arguments map[string]any `json:"arguments,omitempty"`
}
type FunctionCall struct {
@@ -36,7 +36,13 @@ type UsageInfo struct {
}
type LLMProvider interface {
- Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error)
+ Chat(
+ ctx context.Context,
+ messages []Message,
+ tools []ToolDefinition,
+ model string,
+ options map[string]any,
+ ) (*LLMResponse, error)
GetDefaultModel() string
}
@@ -46,7 +52,7 @@ type ToolDefinition struct {
}
type ToolFunctionDefinition struct {
- Name string `json:"name"`
- Description string `json:"description"`
- Parameters map[string]interface{} `json:"parameters"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Parameters map[string]any `json:"parameters"`
}
diff --git a/pkg/tools/web.go b/pkg/tools/web.go
index 1f5c58ea5..301e00daf 100644
--- a/pkg/tools/web.go
+++ b/pkg/tools/web.go
@@ -183,11 +183,17 @@ type PerplexitySearchProvider struct {
func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
searchURL := "https://api.perplexity.ai/chat/completions"
- payload := map[string]interface{}{
+ payload := map[string]any{
"model": "sonar",
"messages": []map[string]string{
- {"role": "system", "content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary."},
- {"role": "user", "content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count)},
+ {
+ "role": "system",
+ "content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary.",
+ },
+ {
+ "role": "user",
+ "content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count),
+ },
},
"max_tokens": 1000,
}
@@ -295,15 +301,15 @@ func (t *WebSearchTool) Description() string {
return "Search the web for current information. Returns titles, URLs, and snippets from search results."
}
-func (t *WebSearchTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *WebSearchTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
+ "properties": map[string]any{
+ "query": map[string]any{
"type": "string",
"description": "Search query",
},
- "count": map[string]interface{}{
+ "count": map[string]any{
"type": "integer",
"description": "Number of results (1-10)",
"minimum": 1.0,
@@ -314,7 +320,7 @@ func (t *WebSearchTool) Parameters() map[string]interface{} {
}
}
-func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *WebSearchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
query, ok := args["query"].(string)
if !ok {
return ErrorResult("query is required")
@@ -359,15 +365,15 @@ func (t *WebFetchTool) Description() string {
return "Fetch a URL and extract readable content (HTML to text). Use this to get weather info, news, articles, or any web content."
}
-func (t *WebFetchTool) Parameters() map[string]interface{} {
- return map[string]interface{}{
+func (t *WebFetchTool) Parameters() map[string]any {
+ return map[string]any{
"type": "object",
- "properties": map[string]interface{}{
- "url": map[string]interface{}{
+ "properties": map[string]any{
+ "url": map[string]any{
"type": "string",
"description": "URL to fetch",
},
- "maxChars": map[string]interface{}{
+ "maxChars": map[string]any{
"type": "integer",
"description": "Maximum characters to extract",
"minimum": 100.0,
@@ -377,7 +383,7 @@ func (t *WebFetchTool) Parameters() map[string]interface{} {
}
}
-func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
+func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
urlStr, ok := args["url"].(string)
if !ok {
return ErrorResult("url is required")
@@ -442,7 +448,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
var text, extractor string
if strings.Contains(contentType, "application/json") {
- var jsonData interface{}
+ var jsonData any
if err := json.Unmarshal(body, &jsonData); err == nil {
formatted, _ := json.MarshalIndent(jsonData, "", " ")
text = string(formatted)
@@ -465,7 +471,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
text = text[:maxChars]
}
- result := map[string]interface{}{
+ result := map[string]any{
"url": urlStr,
"status": resp.StatusCode,
"extractor": extractor,
@@ -477,7 +483,13 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
resultJSON, _ := json.MarshalIndent(result, "", " ")
return &ToolResult{
- ForLLM: fmt.Sprintf("Fetched %d bytes from %s (extractor: %s, truncated: %v)", len(text), urlStr, extractor, truncated),
+ ForLLM: fmt.Sprintf(
+ "Fetched %d bytes from %s (extractor: %s, truncated: %v)",
+ len(text),
+ urlStr,
+ extractor,
+ truncated,
+ ),
ForUser: string(resultJSON),
}
}
diff --git a/pkg/tools/web_test.go b/pkg/tools/web_test.go
index 7e6d62213..d999d8958 100644
--- a/pkg/tools/web_test.go
+++ b/pkg/tools/web_test.go
@@ -20,7 +20,7 @@ func TestWebTool_WebFetch_Success(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"url": server.URL,
}
@@ -56,7 +56,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"url": server.URL,
}
@@ -77,7 +77,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"url": "not-a-valid-url",
}
@@ -98,7 +98,7 @@ func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"url": "ftp://example.com/file.txt",
}
@@ -119,7 +119,7 @@ func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
func TestWebTool_WebFetch_MissingURL(t *testing.T) {
tool := NewWebFetchTool(50000)
ctx := context.Background()
- args := map[string]interface{}{}
+ args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -147,7 +147,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
tool := NewWebFetchTool(1000) // Limit to 1000 chars
ctx := context.Background()
- args := map[string]interface{}{
+ args := map[string]any{
"url": server.URL,
}
@@ -159,7 +159,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
}
// ForUser should contain truncated content (not the full 20000 chars)
- resultMap := make(map[string]interface{})
+ resultMap := make(map[string]any)
json.Unmarshal([]byte(result.ForUser), &resultMap)
if text, ok := resultMap["text"].(string); ok {
if len(text) > 1100 { // Allow some margin
@@ -191,7 +191,7 @@ func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
func TestWebTool_WebSearch_MissingQuery(t *testing.T) {
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5})
ctx := context.Background()
- args := map[string]interface{}{}
+ args := map[string]any{}
result := tool.Execute(ctx, args)
@@ -206,13 +206,17 @@ func TestWebTool_WebFetch_HTMLExtraction(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
- w.Write([]byte(`Content
`)) + w.Write( + []byte( + `Content
`, + ), + ) })) defer server.Close() tool := NewWebFetchTool(50000) ctx := context.Background() - args := map[string]interface{}{ + args := map[string]any{ "url": server.URL, } @@ -251,7 +255,8 @@ func TestWebFetchTool_extractText(t *testing.T) { if len(lines) < 2 { t.Errorf("Expected multiple lines, got %d: %q", len(lines), got) } - if !strings.Contains(got, "Title") || !strings.Contains(got, "Paragraph 1") || !strings.Contains(got, "Paragraph 2") { + if !strings.Contains(got, "Title") || !strings.Contains(got, "Paragraph 1") || + !strings.Contains(got, "Paragraph 2") { t.Errorf("Missing expected text: %q", got) } }, @@ -312,7 +317,7 @@ func TestWebFetchTool_extractText(t *testing.T) { func TestWebTool_WebFetch_MissingDomain(t *testing.T) { tool := NewWebFetchTool(50000) ctx := context.Background() - args := map[string]interface{}{ + args := map[string]any{ "url": "https://", } diff --git a/pkg/utils/download.go b/pkg/utils/download.go index 9fa7fbfa7..5d9a13a30 100644 --- a/pkg/utils/download.go +++ b/pkg/utils/download.go @@ -27,7 +27,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request, // Attach context. req = req.WithContext(ctx) - logger.DebugCF("download", "Starting download", map[string]interface{}{ + logger.DebugCF("download", "Starting download", map[string]any{ "url": req.URL.String(), "max_bytes": maxBytes, }) @@ -52,7 +52,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request, } tmpPath := tmpFile.Name() - logger.DebugCF("download", "Streaming to temp file", map[string]interface{}{ + logger.DebugCF("download", "Streaming to temp file", map[string]any{ "path": tmpPath, }) @@ -84,7 +84,7 @@ func DownloadToFile(ctx context.Context, client *http.Client, req *http.Request, return "", fmt.Errorf("failed to close temp file: %w", err) } - logger.DebugCF("download", "Download complete", map[string]interface{}{ + logger.DebugCF("download", "Download complete", map[string]any{ "path": tmpPath, "bytes_written": written, }) diff --git a/pkg/utils/media.go b/pkg/utils/media.go index 2b184f2ec..a34889fb8 100644 --- a/pkg/utils/media.go +++ b/pkg/utils/media.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/sipeed/picoclaw/pkg/logger" ) @@ -65,8 +66,8 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { } mediaDir := filepath.Join(os.TempDir(), "picoclaw_media") - if err := os.MkdirAll(mediaDir, 0700); err != nil { - logger.ErrorCF(opts.LoggerPrefix, "Failed to create media directory", map[string]interface{}{ + if err := os.MkdirAll(mediaDir, 0o700); err != nil { + logger.ErrorCF(opts.LoggerPrefix, "Failed to create media directory", map[string]any{ "error": err.Error(), }) return "" @@ -79,7 +80,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { // Create HTTP request req, err := http.NewRequest("GET", url, nil) if err != nil { - logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]interface{}{ + logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]any{ "error": err.Error(), }) return "" @@ -93,7 +94,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { client := &http.Client{Timeout: opts.Timeout} resp, err := client.Do(req) if err != nil { - logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]interface{}{ + logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]any{ "error": err.Error(), "url": url, }) @@ -102,7 +103,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - logger.ErrorCF(opts.LoggerPrefix, "File download returned non-200 status", map[string]interface{}{ + logger.ErrorCF(opts.LoggerPrefix, "File download returned non-200 status", map[string]any{ "status": resp.StatusCode, "url": url, }) @@ -111,7 +112,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { out, err := os.Create(localPath) if err != nil { - logger.ErrorCF(opts.LoggerPrefix, "Failed to create local file", map[string]interface{}{ + logger.ErrorCF(opts.LoggerPrefix, "Failed to create local file", map[string]any{ "error": err.Error(), }) return "" @@ -121,13 +122,13 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { if _, err := io.Copy(out, resp.Body); err != nil { out.Close() os.Remove(localPath) - logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]interface{}{ + logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]any{ "error": err.Error(), }) return "" } - logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]interface{}{ + logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]any{ "path": localPath, }) diff --git a/pkg/utils/zip.go b/pkg/utils/zip.go index cad91e420..919ce5a20 100644 --- a/pkg/utils/zip.go +++ b/pkg/utils/zip.go @@ -22,13 +22,13 @@ func ExtractZipFile(zipPath string, targetDir string) error { } defer reader.Close() - logger.DebugCF("zip", "Extracting ZIP", map[string]interface{}{ + logger.DebugCF("zip", "Extracting ZIP", map[string]any{ "zip_path": zipPath, "target_dir": targetDir, "entries": len(reader.File), }) - if err := os.MkdirAll(targetDir, 0755); err != nil { + if err := os.MkdirAll(targetDir, 0o755); err != nil { return fmt.Errorf("failed to create target dir: %w", err) } @@ -43,7 +43,8 @@ func ExtractZipFile(zipPath string, targetDir string) error { // Double-check the resolved path is within target directory (defense-in-depth). targetDirClean := filepath.Clean(targetDir) - if !strings.HasPrefix(filepath.Clean(destPath), targetDirClean+string(filepath.Separator)) && filepath.Clean(destPath) != targetDirClean { + if !strings.HasPrefix(filepath.Clean(destPath), targetDirClean+string(filepath.Separator)) && + filepath.Clean(destPath) != targetDirClean { return fmt.Errorf("zip entry escapes target dir: %q", f.Name) } @@ -55,14 +56,14 @@ func ExtractZipFile(zipPath string, targetDir string) error { } if f.FileInfo().IsDir() { - if err := os.MkdirAll(destPath, 0755); err != nil { + if err := os.MkdirAll(destPath, 0o755); err != nil { return err } continue } // Ensure parent directory exists. - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { return err } @@ -98,7 +99,7 @@ func extractSingleFile(f *zip.File, destPath string) error { defer func() { if cerr := outFile.Close(); cerr != nil { _ = os.Remove(destPath) - logger.ErrorCF("zip", "Failed to close file", map[string]interface{}{ + logger.ErrorCF("zip", "Failed to close file", map[string]any{ "dest_path": destPath, "error": cerr.Error(), }) diff --git a/pkg/voice/transcriber.go b/pkg/voice/transcriber.go index 9af2ea6bb..f973e77fe 100644 --- a/pkg/voice/transcriber.go +++ b/pkg/voice/transcriber.go @@ -29,7 +29,7 @@ type TranscriptionResponse struct { } func NewGroqTranscriber(apiKey string) *GroqTranscriber { - logger.DebugCF("voice", "Creating Groq transcriber", map[string]interface{}{"has_api_key": apiKey != ""}) + logger.DebugCF("voice", "Creating Groq transcriber", map[string]any{"has_api_key": apiKey != ""}) apiBase := "https://api.groq.com/openai/v1" return &GroqTranscriber{ @@ -42,22 +42,22 @@ func NewGroqTranscriber(apiKey string) *GroqTranscriber { } func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string) (*TranscriptionResponse, error) { - logger.InfoCF("voice", "Starting transcription", map[string]interface{}{"audio_file": audioFilePath}) + logger.InfoCF("voice", "Starting transcription", map[string]any{"audio_file": audioFilePath}) audioFile, err := os.Open(audioFilePath) if err != nil { - logger.ErrorCF("voice", "Failed to open audio file", map[string]interface{}{"path": audioFilePath, "error": err}) + logger.ErrorCF("voice", "Failed to open audio file", map[string]any{"path": audioFilePath, "error": err}) return nil, fmt.Errorf("failed to open audio file: %w", err) } defer audioFile.Close() fileInfo, err := audioFile.Stat() if err != nil { - logger.ErrorCF("voice", "Failed to get file info", map[string]interface{}{"path": audioFilePath, "error": err}) + logger.ErrorCF("voice", "Failed to get file info", map[string]any{"path": audioFilePath, "error": err}) return nil, fmt.Errorf("failed to get file info: %w", err) } - logger.DebugCF("voice", "Audio file details", map[string]interface{}{ + logger.DebugCF("voice", "Audio file details", map[string]any{ "size_bytes": fileInfo.Size(), "file_name": filepath.Base(audioFilePath), }) @@ -67,44 +67,44 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string) part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath)) if err != nil { - logger.ErrorCF("voice", "Failed to create form file", map[string]interface{}{"error": err}) + logger.ErrorCF("voice", "Failed to create form file", map[string]any{"error": err}) return nil, fmt.Errorf("failed to create form file: %w", err) } copied, err := io.Copy(part, audioFile) if err != nil { - logger.ErrorCF("voice", "Failed to copy file content", map[string]interface{}{"error": err}) + logger.ErrorCF("voice", "Failed to copy file content", map[string]any{"error": err}) return nil, fmt.Errorf("failed to copy file content: %w", err) } - logger.DebugCF("voice", "File copied to request", map[string]interface{}{"bytes_copied": copied}) + logger.DebugCF("voice", "File copied to request", map[string]any{"bytes_copied": copied}) - if err := writer.WriteField("model", "whisper-large-v3"); err != nil { - logger.ErrorCF("voice", "Failed to write model field", map[string]interface{}{"error": err}) + if err = writer.WriteField("model", "whisper-large-v3"); err != nil { + logger.ErrorCF("voice", "Failed to write model field", map[string]any{"error": err}) return nil, fmt.Errorf("failed to write model field: %w", err) } - if err := writer.WriteField("response_format", "json"); err != nil { - logger.ErrorCF("voice", "Failed to write response_format field", map[string]interface{}{"error": err}) + if err = writer.WriteField("response_format", "json"); err != nil { + logger.ErrorCF("voice", "Failed to write response_format field", map[string]any{"error": err}) return nil, fmt.Errorf("failed to write response_format field: %w", err) } - if err := writer.Close(); err != nil { - logger.ErrorCF("voice", "Failed to close multipart writer", map[string]interface{}{"error": err}) + if err = writer.Close(); err != nil { + logger.ErrorCF("voice", "Failed to close multipart writer", map[string]any{"error": err}) return nil, fmt.Errorf("failed to close multipart writer: %w", err) } url := t.apiBase + "/audio/transcriptions" req, err := http.NewRequestWithContext(ctx, "POST", url, &requestBody) if err != nil { - logger.ErrorCF("voice", "Failed to create request", map[string]interface{}{"error": err}) + logger.ErrorCF("voice", "Failed to create request", map[string]any{"error": err}) return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+t.apiKey) - logger.DebugCF("voice", "Sending transcription request to Groq API", map[string]interface{}{ + logger.DebugCF("voice", "Sending transcription request to Groq API", map[string]any{ "url": url, "request_size_bytes": requestBody.Len(), "file_size_bytes": fileInfo.Size(), @@ -112,37 +112,37 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string) resp, err := t.httpClient.Do(req) if err != nil { - logger.ErrorCF("voice", "Failed to send request", map[string]interface{}{"error": err}) + logger.ErrorCF("voice", "Failed to send request", map[string]any{"error": err}) return nil, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - logger.ErrorCF("voice", "Failed to read response", map[string]interface{}{"error": err}) + logger.ErrorCF("voice", "Failed to read response", map[string]any{"error": err}) return nil, fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { - logger.ErrorCF("voice", "API error", map[string]interface{}{ + logger.ErrorCF("voice", "API error", map[string]any{ "status_code": resp.StatusCode, "response": string(body), }) return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } - logger.DebugCF("voice", "Received response from Groq API", map[string]interface{}{ + logger.DebugCF("voice", "Received response from Groq API", map[string]any{ "status_code": resp.StatusCode, "response_size_bytes": len(body), }) var result TranscriptionResponse if err := json.Unmarshal(body, &result); err != nil { - logger.ErrorCF("voice", "Failed to unmarshal response", map[string]interface{}{"error": err}) + logger.ErrorCF("voice", "Failed to unmarshal response", map[string]any{"error": err}) return nil, fmt.Errorf("failed to unmarshal response: %w", err) } - logger.InfoCF("voice", "Transcription completed successfully", map[string]interface{}{ + logger.InfoCF("voice", "Transcription completed successfully", map[string]any{ "text_length": len(result.Text), "language": result.Language, "duration_seconds": result.Duration, @@ -154,6 +154,6 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string) func (t *GroqTranscriber) IsAvailable() bool { available := t.apiKey != "" - logger.DebugCF("voice", "Checking transcriber availability", map[string]interface{}{"available": available}) + logger.DebugCF("voice", "Checking transcriber availability", map[string]any{"available": available}) return available }