• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smallnest / goclaw / 22518818095

28 Feb 2026 10:18AM UTC coverage: 8.86% (-0.02%) from 8.879%
22518818095

push

github

smallnest
fix cron

12 of 538 new or added lines in 30 files covered. (2.23%)

9 existing lines in 8 files now uncovered.

2868 of 32371 relevant lines covered (8.86%)

0.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

9.86
/cli/root.go
1
package cli
2

3
import (
4
        "context"
5
        "fmt"
6
        "os"
7
        "os/signal"
8
        "syscall"
9
        "time"
10

11
        "github.com/smallnest/goclaw/acp"
12
        "github.com/smallnest/goclaw/agent"
13
        "github.com/smallnest/goclaw/agent/tools"
14
        "github.com/smallnest/goclaw/bus"
15
        "github.com/smallnest/goclaw/channels"
16
        "github.com/smallnest/goclaw/cli/commands"
17
        "github.com/smallnest/goclaw/config"
18
        "github.com/smallnest/goclaw/cron"
19
        "github.com/smallnest/goclaw/gateway"
20
        "github.com/smallnest/goclaw/internal"
21
        "github.com/smallnest/goclaw/internal/logger"
22
        "github.com/smallnest/goclaw/internal/workspace"
23
        "github.com/smallnest/goclaw/providers"
24
        "github.com/smallnest/goclaw/session"
25
        "github.com/spf13/cobra"
26
        "go.uber.org/zap"
27
)
28

29
// Version information (populated by goreleaser)
30
var Version = "dev"
31

32
var rootCmd = &cobra.Command{
33
        Use:   "goclaw",
34
        Short: "Go-based AI Agent framework",
35
        Long:  `goclaw is a Go language implementation of an AI Agent framework, inspired by nanobot.`,
36
}
37

38
var versionCmd = &cobra.Command{
39
        Use:   "version",
40
        Short: "Print version information",
41
        Run:   runVersion,
42
}
43

44
var startCmd = &cobra.Command{
45
        Use:   "start",
46
        Short: "Start the goclaw agent",
47
        Run:   runStart,
48
}
49

50
var configCmd = &cobra.Command{
51
        Use:   "config",
52
        Short: "Configuration management",
53
}
54

55
var configShowCmd = &cobra.Command{
56
        Use:   "show",
57
        Short: "Show current configuration",
58
        Run:   runConfigShow,
59
}
60

61
var installCmd = &cobra.Command{
62
        Use:   "install",
63
        Short: "Install goclaw workspace templates",
64
        Run:   runInstall,
65
}
66

67
// Flags for install command
68
var (
69
        installConfigPath    string
70
        installWorkspacePath string
71
)
72

73
// Flags for start command
74
var (
75
        logLevel string
76
)
77

78
func init() {
1✔
79
        // Add install command flags
1✔
80
        installCmd.Flags().StringVar(&installConfigPath, "config", "", "Path to config file")
1✔
81
        installCmd.Flags().StringVar(&installWorkspacePath, "workspace", "", "Path to workspace directory (overrides config)")
1✔
82

1✔
83
        // Add start command flags
1✔
84
        startCmd.Flags().StringVarP(&logLevel, "log-level", "l", "info", "Log level: debug, info, warn, error, fatal (default: info)")
1✔
85

1✔
86
        rootCmd.AddCommand(versionCmd)
1✔
87
        rootCmd.AddCommand(startCmd)
1✔
88
        rootCmd.AddCommand(installCmd)
1✔
89
        rootCmd.AddCommand(configCmd)
1✔
90
        configCmd.AddCommand(configShowCmd)
1✔
91
        rootCmd.AddCommand(agentsCmd)
1✔
92
        rootCmd.AddCommand(agentCmd)
1✔
93
        rootCmd.AddCommand(sessionsCmd)
1✔
94
        rootCmd.AddCommand(onboardCmd)
1✔
95

1✔
96
        // Register memory and logs commands from commands package
1✔
97
        // Note: skills command is already registered in cli/skills.go
1✔
98
        rootCmd.AddCommand(commands.MemoryCmd)
1✔
99
        rootCmd.AddCommand(commands.LogsCmd)
1✔
100

1✔
101
        // Register browser, tui, gateway, health, status commands
1✔
102
        rootCmd.AddCommand(commands.BrowserCommand())
1✔
103
        rootCmd.AddCommand(commands.TUICommand())
1✔
104
        rootCmd.AddCommand(commands.GatewayCommand())
1✔
105
        rootCmd.AddCommand(commands.HealthCommand())
1✔
106
        rootCmd.AddCommand(commands.StatusCommand())
1✔
107
        rootCmd.AddCommand(commands.ChannelsCommand())
1✔
108

1✔
109
        // Register approvals, cron, system commands (registered via init)
1✔
110
        // These commands auto-register themselves
1✔
111
}
1✔
112

113
// SetVersion sets the version from main package
114
func SetVersion(v string) {
×
115
        Version = v
×
116
        rootCmd.Version = v
×
117
}
×
118

119
// Execute 执行 CLI
120
func Execute() error {
×
121
        return rootCmd.Execute()
×
122
}
×
123

124
// runStart 启动 Agent
125
func runStart(cmd *cobra.Command, args []string) {
×
126
        // 确保内置技能被复制到用户目录
×
127
        if err := internal.EnsureBuiltinSkills(); err != nil {
×
128
                fmt.Fprintf(os.Stderr, "Warning: Failed to ensure builtin skills: %v\n", err)
×
129
        }
×
130

131
        // 确保配置文件存在
132
        configCreated, err := internal.EnsureConfig()
×
133
        if err != nil {
×
134
                fmt.Fprintf(os.Stderr, "Warning: Failed to ensure config: %v\n", err)
×
135
        }
×
136
        if configCreated {
×
137
                fmt.Println("Config file created at: " + internal.GetConfigPath())
×
138
                fmt.Println("Please edit the config file to set your API keys and other settings.")
×
139
                fmt.Println()
×
140
        }
×
141

142
        // 加载配置
143
        cfg, err := config.Load("")
×
144
        if err != nil {
×
145
                fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
×
146
                os.Exit(1)
×
147
        }
×
148

149
        // 初始化日志
150
        if err := logger.Init(logLevel, false); err != nil {
×
151
                fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
×
152
                os.Exit(1)
×
153
        }
×
154
        defer func() { _ = logger.Sync() }()
×
155

156
        logger.Info("Starting goclaw agent")
×
157

×
158
        // 验证配置
×
159
        if err := config.Validate(cfg); err != nil {
×
160
                logger.Fatal("Invalid configuration", zap.Error(err))
×
161
        }
×
162

163
        // 获取 workspace 目录
164
        workspaceDir, err := config.GetWorkspacePath(cfg)
×
165
        if err != nil {
×
166
                logger.Fatal("Failed to get workspace path", zap.Error(err))
×
167
        }
×
168

169
        // 创建 workspace 管理器并确保文件存在
170
        workspaceMgr := workspace.NewManager(workspaceDir)
×
171
        if err := workspaceMgr.Ensure(); err != nil {
×
172
                logger.Warn("Failed to ensure workspace files", zap.Error(err))
×
173
        } else {
×
174
                logger.Info("Workspace ready", zap.String("path", workspaceDir))
×
175
        }
×
176

177
        // 创建消息总线
178
        messageBus := bus.NewMessageBus(100)
×
179
        defer messageBus.Close()
×
180

×
181
        // 创建会话管理器
×
182
        homeDir, err := os.UserHomeDir()
×
183
        if err != nil {
×
184
                logger.Fatal("Failed to get home directory", zap.Error(err))
×
185
        }
×
186
        sessionDir := homeDir + "/.goclaw/sessions"
×
187
        sessionMgr, err := session.NewManager(sessionDir)
×
188
        if err != nil {
×
189
                logger.Fatal("Failed to create session manager", zap.Error(err))
×
190
        }
×
191

192
        // 创建记忆存储
193
        memoryStore := agent.NewMemoryStore(workspaceDir)
×
194

×
195
        // 创建上下文构建器
×
196
        contextBuilder := agent.NewContextBuilder(memoryStore, workspaceDir)
×
197

×
198
        // 创建工具注册表
×
199
        toolRegistry := agent.NewToolRegistry()
×
200

×
201
        // 创建技能加载器
×
202
        // 加载顺序(后加载的同名技能会覆盖前面的):
×
203
        // 1. ./skills/ (当前目录,最高优先级)
×
204
        // 2. ${WORKSPACE}/skills/ (工作区目录)
×
205
        // 3. ~/.goclaw/skills/ (用户全局目录)
×
206
        goclawDir := homeDir + "/.goclaw"
×
207
        globalSkillsDir := goclawDir + "/skills"
×
208
        workspaceSkillsDir := workspaceDir + "/skills"
×
209
        currentSkillsDir := "./skills"
×
210

×
211
        skillsLoader := agent.NewSkillsLoader(goclawDir, []string{
×
212
                globalSkillsDir,    // 最先加载(最低优先级)
×
213
                workspaceSkillsDir, // 其次加载
×
214
                currentSkillsDir,   // 最后加载(最高优先级)
×
215
        })
×
216
        if err := skillsLoader.Discover(); err != nil {
×
217
                logger.Warn("Failed to discover skills", zap.Error(err))
×
218
        } else {
×
219
                skills := skillsLoader.List()
×
220
                if len(skills) > 0 {
×
221
                        logger.Info("Skills loaded", zap.Int("count", len(skills)))
×
222
                }
×
223
        }
224

225
        // 注册文件系统工具
226
        fsTool := tools.NewFileSystemTool(cfg.Tools.FileSystem.AllowedPaths, cfg.Tools.FileSystem.DeniedPaths, workspaceDir)
×
227
        for _, tool := range fsTool.GetTools() {
×
228
                if err := toolRegistry.RegisterExisting(tool); err != nil {
×
229
                        logger.Warn("Failed to register tool", zap.String("tool", tool.Name()))
×
230
                }
×
231
        }
232

233
        // 注册 use_skill 工具(用于两阶段技能加载)
234
        if err := toolRegistry.RegisterExisting(tools.NewUseSkillTool()); err != nil {
×
235
                logger.Warn("Failed to register use_skill tool", zap.Error(err))
×
236
        }
×
237

238
        // 注册 Shell 工具
239
        shellTool := tools.NewShellTool(
×
240
                cfg.Tools.Shell.Enabled,
×
241
                cfg.Tools.Shell.AllowedCmds,
×
242
                cfg.Tools.Shell.DeniedCmds,
×
243
                cfg.Tools.Shell.Timeout,
×
244
                cfg.Tools.Shell.WorkingDir,
×
245
                cfg.Tools.Shell.Sandbox,
×
246
        )
×
247
        for _, tool := range shellTool.GetTools() {
×
248
                if err := toolRegistry.RegisterExisting(tool); err != nil {
×
249
                        logger.Warn("Failed to register tool", zap.String("tool", tool.Name()))
×
250
                }
×
251
        }
252

253
        // 注册 Web 工具
254
        webTool := tools.NewWebTool(
×
255
                cfg.Tools.Web.SearchAPIKey,
×
256
                cfg.Tools.Web.SearchEngine,
×
257
                cfg.Tools.Web.Timeout,
×
258
        )
×
259
        for _, tool := range webTool.GetTools() {
×
260
                if err := toolRegistry.RegisterExisting(tool); err != nil {
×
261
                        logger.Warn("Failed to register tool", zap.String("tool", tool.Name()))
×
262
                }
×
263
        }
264

265
        // 注册智能搜索工具(支持 web search 失败时自动回退到 Google browser 搜索)
266
        browserTimeout := 30
×
267
        if cfg.Tools.Browser.Timeout > 0 {
×
268
                browserTimeout = cfg.Tools.Browser.Timeout
×
269
        }
×
270
        if err := toolRegistry.RegisterExisting(tools.NewSmartSearch(webTool, true, browserTimeout).GetTool()); err != nil {
×
271
                logger.Warn("Failed to register smart_search tool", zap.Error(err))
×
272
        }
×
273

274
        // 注册浏览器工具(如果启用)
275
        if cfg.Tools.Browser.Enabled {
×
276
                browserTool := tools.NewBrowserTool(
×
277
                        cfg.Tools.Browser.Headless,
×
278
                        cfg.Tools.Browser.Timeout,
×
279
                )
×
280
                for _, tool := range browserTool.GetTools() {
×
281
                        if err := toolRegistry.RegisterExisting(tool); err != nil {
×
282
                                logger.Warn("Failed to register tool", zap.String("tool", tool.Name()))
×
283
                        }
×
284
                }
285
                logger.Info("Browser tools registered")
×
286
        }
287

288
        // 注册 Cron 工具
289
        // 注意:cronTool 将在创建 cronService 后注册
290

291
        // 注意: ACP工具(spawn_acp)由agent.Manager内部直接使用
292
        // 不需要通过toolRegistry注册,因为它是agent.Tool类型而不是tools.Tool类型
293

294
        // 创建 LLM 提供商
295
        provider, err := providers.NewProvider(cfg)
×
296
        if err != nil {
×
297
                logger.Fatal("Failed to create LLM provider", zap.Error(err))
×
298
        }
×
299
        defer provider.Close()
×
300

×
301
        // 创建上下文
×
302
        ctx, cancel := context.WithCancel(context.Background())
×
303
        defer cancel()
×
304

×
305
        // 创建通道管理器
×
306
        channelMgr := channels.NewManager(messageBus)
×
307
        if err := channelMgr.SetupFromConfig(cfg); err != nil {
×
308
                logger.Warn("Failed to setup channels from config", zap.Error(err))
×
309
        }
×
310

311
        // 创建 Cron 服务(需要在 Gateway 之前创建,因为 Handler 需要 cronService)
312
        cronService, err := cron.NewService(cron.DefaultCronConfig(), messageBus)
×
313
        if err != nil {
×
314
                logger.Warn("Failed to create cron service", zap.Error(err))
×
315
        }
×
316
        if cronService != nil {
×
317
                if err := cronService.Start(ctx); err != nil {
×
318
                        logger.Warn("Failed to start cron service", zap.Error(err))
×
319
                }
×
320
                defer func() { _ = cronService.Stop() }()
×
321
        }
322

323
        // 注册 Cron 工具(使用已创建并启动的 cronService)
NEW
324
        if cfg.Tools.Cron.Enabled {
×
NEW
325
                logger.Info("Registering cron tools",
×
NEW
326
                        zap.Bool("cron_service_nil", cronService == nil))
×
NEW
327
                cronTool := tools.NewCronTool(cronService)
×
NEW
328
                tools := cronTool.GetTools()
×
NEW
329
                logger.Info("CronTool.GetTools returned",
×
NEW
330
                        zap.Int("count", len(tools)))
×
NEW
331
                for _, tool := range tools {
×
NEW
332
                        if err := toolRegistry.RegisterExisting(tool); err != nil {
×
NEW
333
                                logger.Warn("Failed to register tool", zap.String("tool", tool.Name()), zap.Error(err))
×
NEW
334
                        } else {
×
NEW
335
                                logger.Info("Tool registered successfully", zap.String("tool", tool.Name()))
×
NEW
336
                        }
×
337
                }
NEW
338
                logger.Info("Cron tools registration completed")
×
339
        }
340

341
        // 创建 ACP 管理器(如果启用)
342
        var acpMgr *acp.Manager
×
343
        if cfg.ACP.Enabled {
×
344
                // Use the global ACP manager singleton
×
345
                acpMgr = acp.GetOrCreateGlobalManager(cfg)
×
346
                logger.Info("ACP manager created")
×
347
                toolRegistry.RegisterAgentTool(agent.NewSpawnAcpTool(cfg, acpMgr))
×
348

×
349
                // 创建 thread binding service 并设置到 channel manager
×
350
                threadBindingService := channels.NewThreadBindingService(cfg, sessionMgr)
×
351
                channelMgr.SetThreadBindingService(threadBindingService)
×
352

×
353
                // 创建 ACP router 并设置
×
354
                acpRouter := acp.NewAcpSessionRouter(acpMgr)
×
355
                acpRouter.SetThreadBindingService(threadBindingService)
×
356
                channelMgr.SetAcpRouter(acpRouter)
×
357

×
358
                // 将 thread binding service 也设置到 ACP manager (用于spawn时使用)
×
359
                // 通过一个全局的方式,让spawn能访问到这个service
×
360
                acp.SetGlobalThreadBindingService(threadBindingService)
×
361

×
362
                // Periodically cleanup expired thread bindings.
×
363
                go func() {
×
364
                        ticker := time.NewTicker(time.Minute)
×
365
                        defer ticker.Stop()
×
366
                        for {
×
367
                                select {
×
368
                                case <-ctx.Done():
×
369
                                        return
×
370
                                case <-ticker.C:
×
371
                                        expired := threadBindingService.CleanupExpired()
×
372
                                        if expired > 0 {
×
373
                                                logger.Info("Cleaned up expired ACP thread bindings", zap.Int("count", expired))
×
374
                                        }
×
375
                                }
376
                        }
377
                }()
378
        }
379

380
        // 创建网关服务器
381
        gatewayServer := gateway.NewServer(cfg, messageBus, channelMgr, sessionMgr, cronService, acpMgr)
×
382
        if err := gatewayServer.Start(ctx); err != nil {
×
383
                logger.Warn("Failed to start gateway server", zap.Error(err))
×
384
        }
×
385
        defer func() { _ = gatewayServer.Stop() }()
×
386

387
        // 创建 AgentManager
388
        agentManager := agent.NewAgentManager(&agent.NewAgentManagerConfig{
×
389
                Bus:            messageBus,
×
390
                Provider:       provider,
×
391
                SessionMgr:     sessionMgr,
×
392
                Tools:          toolRegistry,
×
393
                DataDir:        workspaceDir, // 使用 workspace 作为数据目录
×
394
                ContextBuilder: contextBuilder,
×
395
                SkillsLoader:   skillsLoader,
×
396
                ChannelMgr:     channelMgr,
×
397
                AcpManager:     acpMgr,
×
398
        })
×
399

×
400
        // 从配置设置 Agent 和绑定
×
401
        if err := agentManager.SetupFromConfig(cfg, contextBuilder); err != nil {
×
402
                logger.Fatal("Failed to setup agent manager", zap.Error(err))
×
403
        }
×
404

405
        // 处理信号
406
        sigChan := make(chan os.Signal, 1)
×
407
        signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
×
408

×
409
        // 启动通道
×
410
        if err := channelMgr.Start(ctx); err != nil {
×
411
                logger.Error("Failed to start channels", zap.Error(err))
×
412
        }
×
413
        defer func() { _ = channelMgr.Stop() }()
×
414

415
        // 启动出站消息分发
416
        go func() {
×
417
                defer func() {
×
418
                        if r := recover(); r != nil {
×
419
                                logger.Error("Outbound message dispatcher panicked",
×
420
                                        zap.Any("panic", r))
×
421
                        }
×
422
                }()
423
                if err := channelMgr.DispatchOutbound(ctx); err != nil {
×
424
                        logger.Error("Outbound message dispatcher exited with error", zap.Error(err))
×
425
                } else {
×
426
                        logger.Debug("Outbound message dispatcher exited normally")
×
427
                }
×
428
        }()
429

430
        // 启动 AgentManager
431
        go func() {
×
432
                if err := agentManager.Start(ctx); err != nil {
×
433
                        logger.Error("AgentManager error", zap.Error(err))
×
434
                }
×
435
        }()
436

437
        // 等待信号
438
        <-sigChan
×
439
        logger.Info("Received shutdown signal")
×
440

×
441
        // 停止 AgentManager
×
442
        if err := agentManager.Stop(); err != nil {
×
443
                logger.Error("Failed to stop agent manager", zap.Error(err))
×
444
        }
×
445

446
        logger.Info("goclaw agent stopped")
×
447
}
448

449
// runConfigShow 显示配置
450
func runConfigShow(cmd *cobra.Command, args []string) {
×
451
        cfg, err := config.Load("")
×
452
        if err != nil {
×
453
                fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
×
454
                os.Exit(1)
×
455
        }
×
456

457
        fmt.Println("Current Configuration:")
×
458
        fmt.Printf("  Model: %s\n", cfg.Agents.Defaults.Model)
×
459
        fmt.Printf("  Max Iterations: %d\n", cfg.Agents.Defaults.MaxIterations)
×
460
        fmt.Printf("  Temperature: %.1f\n", cfg.Agents.Defaults.Temperature)
×
461
}
462

463
// runInstall 安装 goclaw workspace 模板
464
func runInstall(cmd *cobra.Command, args []string) {
×
465
        // 加载配置
×
466
        cfg, err := config.Load(installConfigPath)
×
467
        if err != nil {
×
468
                fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
×
469
                os.Exit(1)
×
470
        }
×
471

472
        // 获取 workspace 目录
473
        workspaceDir := installWorkspacePath
×
474
        if workspaceDir == "" {
×
475
                workspaceDir, err = config.GetWorkspacePath(cfg)
×
476
                if err != nil {
×
477
                        fmt.Fprintf(os.Stderr, "Failed to get workspace path: %v\n", err)
×
478
                        os.Exit(1)
×
479
                }
×
480
        }
481

482
        // 创建 workspace 管理器并确保文件存在
483
        workspaceMgr := workspace.NewManager(workspaceDir)
×
484
        if err := workspaceMgr.Ensure(); err != nil {
×
485
                fmt.Fprintf(os.Stderr, "Failed to ensure workspace: %v\n", err)
×
486
                os.Exit(1)
×
487
        }
×
488

489
        fmt.Printf("Workspace installed successfully at: %s\n", workspaceDir)
×
490
        fmt.Println("\nWorkspace files:")
×
491
        files, err := workspaceMgr.ListFiles()
×
492
        if err != nil {
×
493
                fmt.Fprintf(os.Stderr, "Failed to list files: %v\n", err)
×
494
                return
×
495
        }
×
496
        for _, f := range files {
×
497
                fmt.Printf("  - %s\n", f)
×
498
        }
×
499

500
        memoryFiles, err := workspaceMgr.ListMemoryFiles()
×
501
        if err == nil && len(memoryFiles) > 0 {
×
502
                fmt.Println("\nMemory files:")
×
503
                for _, f := range memoryFiles {
×
504
                        fmt.Printf("  - memory/%s\n", f)
×
505
                }
×
506
        }
507

508
        fmt.Println("\nYou can now customize these files to define your agent's personality and behavior.")
×
509
}
510

511
// runVersion prints version information
512
func runVersion(cmd *cobra.Command, args []string) {
×
513
        fmt.Printf("goclaw %s\n", Version)
×
514
        fmt.Println("Copyright (c) 2024 smallnest")
×
515
        fmt.Println("License: MIT")
×
516
        fmt.Println("https://github.com/smallnest/goclaw")
×
517
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc