-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
433 lines (370 loc) · 13.6 KB
/
Copy pathmain.go
File metadata and controls
433 lines (370 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
package main
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"time"
"codeactor/internal/agents"
"codeactor/internal/app"
"codeactor/internal/config"
"codeactor/internal/datamanager"
"codeactor/internal/http"
"codeactor/internal/llm"
"codeactor/internal/logging"
messaging "codeactor/internal/messaging"
tuiMsg "codeactor/internal/messaging/consumers"
"codeactor/internal/skills"
"codeactor/internal/tui"
"codeactor/internal/util"
"github.com/spf13/cobra"
)
// 全局 Cobra flag 绑定变量
var (
taskFile string
taskPrompt string
disableAgents string
httpPort int
yoloMode bool
fullYoloMode bool
forceQuit bool // --force-quit: 强制退出模式,agent_exit 时直接退出,不等待 codeseek 进程安全退出
// Memory JSONL 实时持久化
memoryJSONLEnable bool
memoryJSONLDir string
// Config path override
configPathFlag string
)
// rootCmd 根命令 — 无子命令时默认启动 TUI
var rootCmd = &cobra.Command{
Use: "codeactor",
Short: "CodeActor - AI-powered coding assistant",
Long: `CodeActor is an AI-powered coding assistant that can run in
terminal UI mode or HTTP server mode.`,
Run: func(cmd *cobra.Command, args []string) {
if taskPrompt != "" {
runPrompt(taskPrompt, disableAgents)
} else {
runTUI(taskFile, disableAgents)
}
},
SilenceUsage: true,
}
// tuiCmd 显式 TUI 子命令
var tuiCmd = &cobra.Command{
Use: "tui",
Short: "Start in terminal UI mode (default)",
Run: func(cmd *cobra.Command, args []string) {
runTUI(taskFile, disableAgents)
},
}
// httpCmd HTTP 服务器子命令
var httpCmd = &cobra.Command{
Use: "http",
Short: "Start in HTTP server mode",
Run: func(cmd *cobra.Command, args []string) {
runHTTP(taskFile, disableAgents, httpPort)
},
}
func init() {
// Safety net: silence all slog output until proper initialization in main().
// This prevents any pre-main() logging from corrupting the TUI display.
slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelInfo})))
// Initialize language manager with default language (English)
tui.InitLangManager()
// --- Cobra 命令配置 ---
// 持久 flags(所有子命令可用)
rootCmd.PersistentFlags().StringVarP(&taskFile, "taskfile", "f", "", "Load task from file")
rootCmd.PersistentFlags().StringVarP(&taskPrompt, "prompt", "p", "", "Execute a task prompt directly (non-interactive mode)")
rootCmd.PersistentFlags().StringVarP(&disableAgents, "disable-agents", "d", "", "Disable specified agents (comma-separated)")
// Config path override
rootCmd.PersistentFlags().StringVarP(&configPathFlag, "config", "c", "", "Path to config.toml file (overrides default config path)")
// http 子命令专属 flags
httpCmd.Flags().IntVar(&httpPort, "port", 0, "HTTP server port (0 = auto-detect from 9800)")
// YOLO 模式:跳过所有授权检查
rootCmd.PersistentFlags().BoolVarP(&yoloMode, "yolo", "y", false, "YOLO mode: auto-approve all dangerous operations without user confirmation")
rootCmd.PersistentFlags().BoolVarP(&fullYoloMode, "full-yolo", "Y", false, "FULL-YOLO mode: autonomous mode (implies --yolo), removes ask_user_for_help from all agents, agents make decisions independently")
// ForceQuit 模式:agent_exit 时强制退出,不等待 codeseek 进程安全退出
rootCmd.PersistentFlags().BoolVarP(&forceQuit, "force-quit", "Q", false, "Force quit: exit immediately without waiting for codeseek process to shut down safely")
// Memory JSONL 实时写入
rootCmd.Flags().BoolVar(&memoryJSONLEnable, "memory-jsonl", false, "Enable real-time memory JSONL persistence per agent delegate task")
rootCmd.Flags().StringVar(&memoryJSONLDir, "memory-jsonl-dir", "", "Custom output directory for memory JSONL files (default: ~/.codeactor/data/memory_jsonl/{projectID}/)")
// 注册子命令
rootCmd.AddCommand(tuiCmd)
rootCmd.AddCommand(httpCmd)
}
func main() {
defer util.RecoverPanic()
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func initApp() (string, error) {
repoPath, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get current working directory: %w", err)
}
return repoPath, nil
}
// runTUI 启动终端 UI 模式
func runTUI(taskFile, disableAgents string) {
// Initialize mode-aware logging: file only, never stdout/stderr
if err := logging.Init(logging.ModeTUI); err != nil {
slog.Error("Failed to initialize logging", "error", err)
}
defer logging.Close()
repoPath, err := initApp()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// TUI mode init — matches original switch "tui" case exactly
ctx := context.Background()
configPath := getConfigPath()
slog.Info("Loading configuration", "config_path", configPath)
config, err := llm.LoadConfig(configPath)
if err != nil {
slog.Error("Failed to load configuration", "error", util.WrapError(ctx, err, "main::LoadConfig"))
os.Exit(1)
}
client, err := llm.NewClient(config)
if err != nil {
slog.Error("Failed to create client", "error", util.WrapError(ctx, err, "main::NewClient"))
os.Exit(1)
}
codeActor, err := app.NewCodeActor(client)
if err != nil {
slog.Error("Failed to create coding assistant", "error", util.WrapError(ctx, err, "main::NewCodeActor"))
os.Exit(1)
}
codeActor.SetEmbeddedBinaries(distBinFS)
if err := agents.InitToolLogger(); err != nil {
slog.Warn("Failed to initialize tool logger", "error", err)
}
codeActor.DisabledAgents = disableAgents
codeActor.YoloMode = yoloMode
codeActor.FullYoloMode = fullYoloMode
codeActor.ForceQuit = forceQuit
// Memory JSONL 配置(CLI flag 覆盖配置文件)
if memoryJSONLEnable {
config.MemoryJSONL.Enable = true
}
if memoryJSONLDir != "" {
config.MemoryJSONL.OutputDir = memoryJSONLDir
}
// 主动初始化:启动 codeseek MCP 等核心服务
codeActor.Init(client.Engine, repoPath)
defer codeActor.Close()
// 加载 skills
homeDir, _ := os.UserHomeDir()
projectSkillsDir := filepath.Join(repoPath, ".codeactor", "skills")
homeSkillsDir := filepath.Join(homeDir, ".codeactor", "skills")
var skillRegistry *skills.SkillRegistry
skillRegistry, err = skills.LoadSkills(projectSkillsDir)
if err != nil {
slog.Warn("Failed to load project skills", "path", projectSkillsDir, "error", err)
}
if skillRegistry.Count() == 0 {
skillRegistry, err = skills.LoadSkills(homeSkillsDir)
if err != nil {
slog.Warn("Failed to load home skills", "path", homeSkillsDir, "error", err)
}
}
codeActor.SkillRegistry = skillRegistry
slog.Info("Skill registry loaded", "count", skillRegistry.Count())
taskManager := http.NewTaskManager(nil, config.TaskTimeout)
dataManager, err := datamanager.NewDataManager()
if err != nil {
slog.Error("Failed to initialize DataManager", "error", err)
} else {
dataManager.Start()
}
tui.StartTUI(taskFile, codeActor, taskManager, dataManager, config)
dataManager.FlushAll()
dataManager.Stop()
}
// runHTTP 启动 HTTP 服务器模式
func runHTTP(taskFile, disableAgents string, httpPort int) {
// Initialize mode-aware logging: stderr + file
if err := logging.Init(logging.ModeHTTP); err != nil {
slog.Error("Failed to initialize logging", "error", err)
}
defer logging.Close()
repoPath, err := initApp()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// HTTP mode init — matches original switch "http" case exactly
ctx := context.Background()
logDir := "logs"
if err := os.MkdirAll(logDir, 0755); err != nil {
slog.Error("Failed to create logs directory", "error", util.WrapError(ctx, err, "main::MkdirAll"))
os.Exit(1)
}
logFile, err := os.OpenFile(filepath.Join(logDir, "server.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
slog.Error("Failed to open log file", "error", util.WrapError(ctx, err, "main::OpenFile"))
os.Exit(1)
}
multiWriter := io.MultiWriter(os.Stderr, logFile)
logger := slog.New(slog.NewTextHandler(multiWriter, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
configPath := getConfigPath()
slog.Info("Loading configuration", "config_path", configPath)
config, err := llm.LoadConfig(configPath)
if err != nil {
slog.Error("Failed to load configuration", "error", util.WrapError(ctx, err, "main::LoadConfig"))
os.Exit(1)
}
slog.Info("Creating assistant.client")
client, err := llm.NewClient(config)
if err != nil {
slog.Error("Failed to create assistant.client", "error", util.WrapError(ctx, err, "main::NewClient"))
os.Exit(1)
}
codeActor, err := app.NewCodeActor(client)
if err != nil {
slog.Error("Failed to create coding assistant", "error", util.WrapError(ctx, err, "main::NewCodeActor"))
os.Exit(1)
}
codeActor.SetEmbeddedBinaries(distBinFS)
codeActor.DisabledAgents = disableAgents
codeActor.YoloMode = yoloMode
codeActor.FullYoloMode = fullYoloMode
codeActor.ForceQuit = forceQuit
// Memory JSONL 配置(CLI flag 覆盖配置文件)
if memoryJSONLEnable {
config.MemoryJSONL.Enable = true
}
if memoryJSONLDir != "" {
config.MemoryJSONL.OutputDir = memoryJSONLDir
}
// 主动初始化:启动 codeseek MCP 等核心服务
codeActor.Init(client.Engine, repoPath)
defer codeActor.Close()
messageDispatcher := messaging.NewMessageDispatcher(100)
codeActor.IntegrateMessaging(messageDispatcher)
server := http.NewServer(codeActor)
slog.Info("Starting HTTP server", "port", httpPort)
if err := server.Run(httpPort); err != nil {
slog.Error("Failed to start HTTP server", "error", util.WrapError(ctx, err, "main::ServerRun"))
os.Exit(1)
}
}
// runPrompt 直接执行任务提示(非交互模式),将过程输出到 stdout
func runPrompt(taskPromptStr, disableAgentsStr string) {
// Initialize mode-aware logging: file only, never stdout/stderr
if err := logging.Init(logging.ModeTUI); err != nil {
slog.Error("Failed to initialize logging", "error", err)
}
defer logging.Close()
repoPath, err := initApp()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
ctx := context.Background()
configPath := getConfigPath()
slog.Info("Loading configuration", "config_path", configPath)
config, err := llm.LoadConfig(configPath)
if err != nil {
slog.Error("Failed to load configuration", "error", util.WrapError(ctx, err, "main::LoadConfig"))
os.Exit(1)
}
client, err := llm.NewClient(config)
if err != nil {
slog.Error("Failed to create client", "error", util.WrapError(ctx, err, "main::NewClient"))
os.Exit(1)
}
codeActor, err := app.NewCodeActor(client)
if err != nil {
slog.Error("Failed to create coding assistant", "error", util.WrapError(ctx, err, "main::NewCodeActor"))
os.Exit(1)
}
codeActor.SetEmbeddedBinaries(distBinFS)
if err := agents.InitToolLogger(); err != nil {
slog.Warn("Failed to initialize tool logger", "error", err)
}
codeActor.DisabledAgents = disableAgentsStr
codeActor.YoloMode = yoloMode
codeActor.FullYoloMode = fullYoloMode
codeActor.ForceQuit = forceQuit
// Memory JSONL 配置(CLI flag 覆盖配置文件)
if memoryJSONLEnable {
config.MemoryJSONL.Enable = true
}
if memoryJSONLDir != "" {
config.MemoryJSONL.OutputDir = memoryJSONLDir
}
codeActor.Init(client.Engine, repoPath)
defer codeActor.Close()
// 加载 skills
homeDir, _ := os.UserHomeDir()
projectSkillsDir := filepath.Join(repoPath, ".codeactor", "skills")
homeSkillsDir := filepath.Join(homeDir, ".codeactor", "skills")
var skillRegistry *skills.SkillRegistry
skillRegistry, err = skills.LoadSkills(projectSkillsDir)
if err != nil {
slog.Warn("Failed to load project skills", "path", projectSkillsDir, "error", err)
}
if skillRegistry.Count() == 0 {
skillRegistry, err = skills.LoadSkills(homeSkillsDir)
if err != nil {
slog.Warn("Failed to load home skills", "path", homeSkillsDir, "error", err)
}
}
codeActor.SkillRegistry = skillRegistry
slog.Info("Skill registry loaded", "count", skillRegistry.Count())
// 设置消息总线 - 使用 TUIConsumer 输出到 stdout
dispatcher := messaging.NewMessageDispatcher(100)
defer dispatcher.Shutdown()
publisher := messaging.NewMessagePublisher(dispatcher)
tuiConsumer := tuiMsg.NewTUIConsumer(os.Stdout, publisher)
dispatcher.RegisterConsumer(tuiConsumer)
codeActor.IntegrateMessaging(dispatcher)
// 构建并执行任务
taskID := fmt.Sprintf("prompt-%d", time.Now().UnixMilli())
request := app.NewTaskRequest(ctx, taskID).
WithProjectDir(repoPath).
WithTaskDesc(taskPromptStr).
WithMessagePublisher(publisher)
result, err := codeActor.ProcessCodingTaskWithCallback(request)
if err != nil {
fmt.Fprintf(os.Stderr, "\n❌ Task failed: %v\n", err)
os.Exit(1)
}
// 打印最终结果(仅在 TUIConsumer 未显示的情况下)
if result != "" {
fmt.Printf("\n📋 Task result:\n%s\n", result)
}
}
// getConfigPath 返回配置文件的路径,优先使用 $HOME/.codeactor/config/config.toml
// 如果配置文件不存在,则自动生成默认配置模板
// 如果命令行通过 --config/-c 指定了路径,则直接使用该路径(不自动创建)
func getConfigPath() string {
// 如果命令行指定了 --config,直接使用(不自动创建配置文件)
if configPathFlag != "" {
return configPathFlag
}
// 原有逻辑...
homeDir, err := os.UserHomeDir()
if err != nil {
localPath := "config/config.toml"
if _, err := os.Stat(localPath); os.IsNotExist(err) {
if genErr := config.EnsureConfigExists(localPath); genErr != nil {
panic(fmt.Sprintf("无法创建配置文件: %v", genErr))
}
}
return localPath
}
configDir := filepath.Join(homeDir, ".codeactor", "config")
configPath := filepath.Join(configDir, "config.toml")
if err := config.EnsureConfigExists(configPath); err != nil {
panic(fmt.Sprintf("无法创建配置文件: %v", err))
}
return configPath
}