@dcode-dev/dcode-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/NPM_README.md +78 -0
  2. package/README.md +341 -0
  3. package/bin/dcode-bin +0 -0
  4. package/bin/dcode.js +44 -0
  5. package/cmd/agent_v2.go +448 -0
  6. package/cmd/analyze.go +97 -0
  7. package/cmd/auth.go +338 -0
  8. package/cmd/compose.go +284 -0
  9. package/cmd/context.go +111 -0
  10. package/cmd/edit.go +116 -0
  11. package/cmd/env.go +10 -0
  12. package/cmd/fix.go +145 -0
  13. package/cmd/gemini.go +20 -0
  14. package/cmd/generate.go +47 -0
  15. package/cmd/interactive.go +33 -0
  16. package/cmd/mcp.go +196 -0
  17. package/cmd/patch.go +19 -0
  18. package/cmd/providers.go +67 -0
  19. package/cmd/root.go +41 -0
  20. package/cmd/search.go +61 -0
  21. package/cmd/server.go +36 -0
  22. package/cmd/switch.go +122 -0
  23. package/cmd/terminal.go +277 -0
  24. package/go.mod +42 -0
  25. package/go.sum +86 -0
  26. package/internal/agent/agent.go +332 -0
  27. package/internal/agent/parse.go +25 -0
  28. package/internal/agents/base.go +154 -0
  29. package/internal/agents/documenter.go +77 -0
  30. package/internal/agents/generalist.go +266 -0
  31. package/internal/agents/investigator.go +60 -0
  32. package/internal/agents/registry.go +34 -0
  33. package/internal/agents/reviewer.go +67 -0
  34. package/internal/agents/tester.go +73 -0
  35. package/internal/ai/client.go +634 -0
  36. package/internal/ai/tools.go +332 -0
  37. package/internal/auth/adc.go +108 -0
  38. package/internal/auth/apikey.go +67 -0
  39. package/internal/auth/factory.go +145 -0
  40. package/internal/auth/oauth2.go +227 -0
  41. package/internal/auth/store.go +216 -0
  42. package/internal/auth/types.go +79 -0
  43. package/internal/auth/vertex.go +138 -0
  44. package/internal/config/config.go +428 -0
  45. package/internal/config/policy.go +251 -0
  46. package/internal/context/builder.go +312 -0
  47. package/internal/detector/detector.go +204 -0
  48. package/internal/diffutil/diffutil.go +30 -0
  49. package/internal/fsutil/fsutil.go +35 -0
  50. package/internal/mcp/client.go +314 -0
  51. package/internal/mcp/manager.go +221 -0
  52. package/internal/policy/policy.go +89 -0
  53. package/internal/prompt/interactive.go +338 -0
  54. package/internal/registry/agent.go +201 -0
  55. package/internal/registry/tool.go +181 -0
  56. package/internal/scheduler/scheduler.go +250 -0
  57. package/internal/server/server.go +167 -0
  58. package/internal/tools/file.go +183 -0
  59. package/internal/tools/filesystem.go +286 -0
  60. package/internal/tools/git.go +355 -0
  61. package/internal/tools/memory.go +269 -0
  62. package/internal/tools/registry.go +49 -0
  63. package/internal/tools/search.go +230 -0
  64. package/internal/tools/shell.go +84 -0
  65. package/internal/websearch/search.go +40 -0
  66. package/internal/websearch/tavily.go +79 -0
  67. package/main.go +19 -0
  68. package/package.json +57 -0
  69. package/scripts/install.js +59 -0
  70. package/scripts/uninstall.js +28 -0
@@ -0,0 +1,448 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "os"
7
+ "strings"
8
+ "time"
9
+
10
+ "github.com/ddhanush1/dcode/internal/agents"
11
+ "github.com/ddhanush1/dcode/internal/ai"
12
+ "github.com/ddhanush1/dcode/internal/auth"
13
+ "github.com/ddhanush1/dcode/internal/config"
14
+ "github.com/ddhanush1/dcode/internal/registry"
15
+ "github.com/ddhanush1/dcode/internal/scheduler"
16
+ "github.com/ddhanush1/dcode/internal/tools"
17
+ "github.com/peterh/liner"
18
+ "github.com/spf13/cobra"
19
+ )
20
+
21
+ var agentV2Cmd = &cobra.Command{
22
+ Use: "agent-v2",
23
+ Short: "Start the new agent system (v2 architecture)",
24
+ Long: `Interactive AI agent using the new architecture with agent registry and tool system`,
25
+ RunE: runAgentV2,
26
+ }
27
+
28
+ func init() {
29
+ rootCmd.AddCommand(agentV2Cmd)
30
+
31
+ // Add flags to both root and agent-v2 commands
32
+ for _, cmd := range []*cobra.Command{rootCmd, agentV2Cmd} {
33
+ cmd.Flags().String("agent", "generalist", "Agent to use (generalist, codebase_investigator)")
34
+ cmd.Flags().String("provider", "", "AI provider (openai, gemini, claude)")
35
+ cmd.Flags().String("model", "", "AI model to use")
36
+ }
37
+ }
38
+
39
+ func runAgentV2(cmd *cobra.Command, args []string) error {
40
+ // Load config
41
+ cfg, err := config.LoadConfig()
42
+ if err != nil {
43
+ return fmt.Errorf("failed to load config: %w", err)
44
+ }
45
+
46
+ // Override with flags
47
+ if provider, _ := cmd.Flags().GetString("provider"); provider != "" {
48
+ cfg.Model.Provider = provider
49
+ cfg.Auth.Provider = provider
50
+ }
51
+ if model, _ := cmd.Flags().GetString("model"); model != "" {
52
+ cfg.Model.Model = model
53
+ }
54
+
55
+ // Interactive provider selection if not set
56
+ if cfg.Model.Provider == "" {
57
+ fmt.Println("šŸ¤– Welcome to DCode!")
58
+ fmt.Println("\nSelect your AI provider:")
59
+ fmt.Println(" 1. Gemini (Google)")
60
+ fmt.Println(" 2. OpenAI (ChatGPT)")
61
+ fmt.Println(" 3. GitHub Copilot")
62
+ fmt.Println(" 4. Claude (Anthropic)")
63
+ fmt.Print("\nEnter choice (1-4): ")
64
+
65
+ var choice int
66
+ fmt.Scanf("%d", &choice)
67
+
68
+ providers := map[int]string{
69
+ 1: "gemini",
70
+ 2: "openai",
71
+ 3: "copilot",
72
+ 4: "claude",
73
+ }
74
+
75
+ if provider, ok := providers[choice]; ok {
76
+ cfg.Model.Provider = provider
77
+ cfg.Auth.Provider = provider
78
+
79
+ // Set default models
80
+ switch provider {
81
+ case "gemini":
82
+ cfg.Model.Model = "gemini-2.0-flash-exp"
83
+ case "openai":
84
+ cfg.Model.Model = "gpt-4"
85
+ case "copilot":
86
+ cfg.Model.Model = "gpt-4"
87
+ case "claude":
88
+ cfg.Model.Model = "claude-3-5-sonnet-20241022"
89
+ }
90
+
91
+ config.SaveConfig(cfg)
92
+ fmt.Printf("\nāœ… Set provider to: %s\n\n", provider)
93
+ } else {
94
+ return fmt.Errorf("invalid choice. Use: dcode --provider gemini|openai|copilot|claude")
95
+ }
96
+ }
97
+
98
+ // Create authenticator
99
+ authFactory := auth.NewFactory(auth.GetDefaultStore())
100
+ authenticator, err := authFactory.CreateFromConfig(cfg)
101
+ if err != nil {
102
+ fmt.Printf("āš ļø Warning: Auth setup failed: %v\n", err)
103
+ fmt.Println("Falling back to environment variables...")
104
+ }
105
+
106
+ // Get API key
107
+ var apiKey string
108
+ if authenticator != nil {
109
+ ctx := context.Background()
110
+ apiKey, err = authenticator.GetToken(ctx)
111
+ if err != nil {
112
+ return fmt.Errorf("failed to get API key: %w", err)
113
+ }
114
+ } else {
115
+ // Fallback to env
116
+ switch cfg.Model.Provider {
117
+ case "openai":
118
+ apiKey = os.Getenv("OPENAI_API_KEY")
119
+ case "gemini":
120
+ apiKey = os.Getenv("GEMINI_API_KEY")
121
+ if apiKey == "" {
122
+ apiKey = os.Getenv("GOOGLE_API_KEY")
123
+ }
124
+ case "github":
125
+ apiKey = os.Getenv("GITHUB_TOKEN")
126
+ case "claude":
127
+ apiKey = os.Getenv("ANTHROPIC_API_KEY")
128
+ case "copilot":
129
+ apiKey = os.Getenv("COPILOT_TOKEN")
130
+ if apiKey == "" {
131
+ // Try to get token from Copilot CLI config
132
+ apiKey = ai.GetCopilotToken()
133
+ }
134
+ }
135
+
136
+ if apiKey == "" {
137
+ return fmt.Errorf("API key not found for provider: %s", cfg.Model.Provider)
138
+ }
139
+ }
140
+
141
+ // Create AI client
142
+ aiClient := &ai.Client{
143
+ Provider: ai.Provider(cfg.Model.Provider),
144
+ Model: cfg.Model.Model,
145
+ APIKey: apiKey,
146
+ }
147
+
148
+ // Create registries
149
+ agentRegistry := registry.NewAgentRegistry()
150
+ toolRegistry := registry.NewToolRegistry()
151
+
152
+ // Register tools
153
+ if err := tools.RegisterBuiltinTools(toolRegistry); err != nil {
154
+ return fmt.Errorf("failed to register tools: %w", err)
155
+ }
156
+
157
+ // Register agents
158
+ if err := agents.RegisterAllAgents(agentRegistry, cfg, aiClient); err != nil {
159
+ return fmt.Errorf("failed to register agents: %w", err)
160
+ }
161
+
162
+ // Create policy
163
+ policyFile, err := config.LoadPolicyFile()
164
+ if err != nil {
165
+ return fmt.Errorf("failed to load policy: %w", err)
166
+ }
167
+
168
+ // Create scheduler
169
+ sched := scheduler.NewScheduler(toolRegistry, policyFile)
170
+
171
+ // Create agent manager
172
+ agentManager := agents.NewAgentManager(agentRegistry, toolRegistry)
173
+
174
+ // Switch to requested agent
175
+ agentID, _ := cmd.Flags().GetString("agent")
176
+ if err := agentManager.SwitchAgent(agentID); err != nil {
177
+ return err
178
+ }
179
+
180
+ currentAgent, _ := agentManager.GetCurrentAgent()
181
+
182
+ // Simple welcome message (like gemini-cli)
183
+ fmt.Println()
184
+ fmt.Printf("╭─ DCode v2.0 ─ %s ─ %d tools\n", cfg.Model.Model, len(toolRegistry.List()))
185
+ fmt.Println("╰─ Type your message or /help for commands")
186
+ fmt.Println()
187
+
188
+ // Start REPL
189
+ line := liner.NewLiner()
190
+ defer line.Close()
191
+
192
+ line.SetCtrlCAborts(true)
193
+
194
+ // Load history
195
+ historyFile := os.ExpandEnv("$HOME/.dcode_history")
196
+ if f, err := os.Open(historyFile); err == nil {
197
+ line.ReadHistory(f)
198
+ f.Close()
199
+ }
200
+
201
+ ctx := context.Background()
202
+
203
+ for {
204
+ // Simple prompt like gemini-cli
205
+ prompt := "╰─ "
206
+ input, err := line.Prompt(prompt)
207
+ if err != nil {
208
+ break
209
+ }
210
+
211
+ input = strings.TrimSpace(input)
212
+ if input == "" {
213
+ continue
214
+ }
215
+
216
+ line.AppendHistory(input)
217
+
218
+ // Handle commands
219
+ if strings.HasPrefix(input, "/") {
220
+ if handled, shouldContinue := handleCommand(input, agentManager, toolRegistry, agentRegistry, line, historyFile); !shouldContinue {
221
+ break
222
+ } else if handled {
223
+ currentAgent, _ = agentManager.GetCurrentAgent()
224
+ continue
225
+ }
226
+ }
227
+
228
+ // Get current working directory
229
+ cwd, err := os.Getwd()
230
+ if err != nil {
231
+ cwd = "unknown"
232
+ }
233
+
234
+ // Execute agent with streaming and working directory context
235
+ agentInput := &registry.AgentInput{
236
+ Query: input,
237
+ Parameters: make(map[string]interface{}),
238
+ History: agentManager.GetHistory(currentAgent.ID),
239
+ Tools: currentAgent.Tools,
240
+ ToolRegistry: toolRegistry,
241
+ SystemPrompt: currentAgent.SystemPrompt + fmt.Sprintf("\n\nCURRENT WORKING DIRECTORY: %s\nIMPORTANT: When creating files, use relative paths from the current working directory unless absolute paths are explicitly requested.", cwd),
242
+ Context: make(map[string]interface{}),
243
+ Config: currentAgent.Config,
244
+ }
245
+
246
+ fmt.Println("\nšŸ¤” Thinking...")
247
+
248
+ // Try to execute with streaming if possible
249
+ var output *registry.AgentOutput
250
+ var execErr error
251
+
252
+ // Check if we can use streaming (only for GeneralistAgent currently)
253
+ if genAgent, ok := currentAgent.Instance.(*agents.GeneralistAgent); ok {
254
+ // Stream with typewriter effect
255
+ fmt.Print("\nšŸ’¬ ")
256
+ output, execErr = genAgent.ExecuteStream(agentInput, func(chunk string) {
257
+ for _, char := range chunk {
258
+ fmt.Print(string(char))
259
+ time.Sleep(20 * time.Millisecond) // Typewriter speed
260
+ }
261
+ })
262
+ fmt.Println() // Newline after streaming
263
+ } else {
264
+ // Fallback to non-streaming
265
+ output, execErr = agentManager.Execute(ctx, agentInput)
266
+ if execErr == nil && output.Message != "" {
267
+ fmt.Printf("\nšŸ’¬ %s\n", output.Message)
268
+ }
269
+ }
270
+
271
+ if execErr != nil {
272
+ fmt.Printf("\nāŒ Error: %v\n\n", execErr)
273
+ continue
274
+ }
275
+
276
+ // Execute tool calls if any and continue conversation
277
+ if len(output.ToolCalls) > 0 {
278
+ fmt.Printf("\nšŸ”§ Executing %d tool(s)...\n", len(output.ToolCalls))
279
+
280
+ toolResults := []string{}
281
+ for _, toolCall := range output.ToolCalls {
282
+ fmt.Printf(" • %s\n", toolCall.ToolName)
283
+
284
+ req := &scheduler.ExecutionRequest{
285
+ ToolID: toolCall.ToolName,
286
+ Parameters: toolCall.Parameters,
287
+ Context: make(map[string]interface{}),
288
+ }
289
+
290
+ confirmHandler := func(req *scheduler.ConfirmationRequest) (bool, error) {
291
+ // Use liner.Prompt for input (works with liner's terminal mode)
292
+ response, err := line.Prompt(fmt.Sprintf("āš ļø Confirm %s? (y/n): ", req.ToolName))
293
+ if err != nil {
294
+ return false, err
295
+ }
296
+ response = strings.ToLower(strings.TrimSpace(response))
297
+ return response == "y" || response == "yes", nil
298
+ }
299
+
300
+ result, err := sched.Execute(ctx, req, confirmHandler)
301
+ if err != nil {
302
+ fmt.Printf(" āŒ Failed: %v\n", err)
303
+ toolResults = append(toolResults, fmt.Sprintf("Tool '%s' failed: %v", toolCall.ToolName, err))
304
+ } else if result.Success {
305
+ fmt.Printf(" āœ… Success\n")
306
+ // Format tool result as string
307
+ resultStr := fmt.Sprintf("Tool '%s' result: %v", toolCall.ToolName, result.Data)
308
+ toolResults = append(toolResults, resultStr)
309
+ } else {
310
+ fmt.Printf(" āš ļø %s\n", result.Error)
311
+ toolResults = append(toolResults, fmt.Sprintf("Tool '%s' error: %s", toolCall.ToolName, result.Error))
312
+ }
313
+ }
314
+
315
+ // Continue conversation with tool results
316
+ if len(toolResults) > 0 {
317
+ fmt.Println("\nšŸ¤” Processing results...")
318
+
319
+ // Only add model's response if it has content
320
+ if output.Message != "" {
321
+ agentManager.AddMessage(currentAgent.ID, "model", output.Message, make(map[string]interface{}))
322
+ }
323
+
324
+ // Add function results as special "function" message
325
+ toolResultsStr := strings.Join(toolResults, "\n\n")
326
+ agentManager.AddMessage(currentAgent.ID, "function", toolResultsStr, make(map[string]interface{}))
327
+
328
+ // Get continuation from AI
329
+ continuationInput := &registry.AgentInput{
330
+ Query: "", // Empty query, AI should process function results
331
+ Parameters: make(map[string]interface{}),
332
+ History: agentManager.GetHistory(currentAgent.ID),
333
+ Tools: currentAgent.Tools,
334
+ ToolRegistry: toolRegistry,
335
+ SystemPrompt: currentAgent.SystemPrompt,
336
+ Context: make(map[string]interface{}),
337
+ Config: currentAgent.Config,
338
+ }
339
+
340
+ continuationOutput, err := agentManager.Execute(ctx, continuationInput)
341
+ if err != nil {
342
+ fmt.Printf("\nāŒ Error processing results: %v\n\n", err)
343
+ } else {
344
+ fmt.Printf("\nšŸ’¬ %s\n", continuationOutput.Message)
345
+ }
346
+ }
347
+ }
348
+
349
+ fmt.Println()
350
+ }
351
+
352
+ return nil
353
+ }
354
+
355
+ func handleCommand(input string, agentManager *agents.AgentManager, toolRegistry *registry.ToolRegistry, agentRegistry *registry.AgentRegistry, linerInst *liner.State, historyFile string) (handled bool, shouldContinue bool) {
356
+ parts := strings.Fields(input)
357
+ if len(parts) == 0 {
358
+ return true, true
359
+ }
360
+
361
+ command := parts[0]
362
+
363
+ switch command {
364
+ case "/help", "/h":
365
+ fmt.Println("\nAvailable Commands:")
366
+ fmt.Println(" /help, /h - Show this help")
367
+ fmt.Println(" /agent - List and switch agents")
368
+ fmt.Println(" /tools - List available tools")
369
+ fmt.Println(" /auth - Show authentication status")
370
+ fmt.Println(" /clear - Clear conversation history")
371
+ fmt.Println(" /exit, /quit - Exit the agent")
372
+ fmt.Println()
373
+ return true, true
374
+
375
+ case "/agent", "/agents":
376
+ if len(parts) > 1 {
377
+ // Switch agent
378
+ agentID := parts[1]
379
+ if err := agentManager.SwitchAgent(agentID); err != nil {
380
+ fmt.Printf("āŒ Error: %v\n", err)
381
+ } else {
382
+ agent, _ := agentManager.GetCurrentAgent()
383
+ fmt.Printf("āœ… Switched to: %s\n", agent.Name)
384
+ }
385
+ } else {
386
+ // List agents
387
+ agents := agentRegistry.List()
388
+ fmt.Println("\nAvailable Agents:")
389
+ for _, agent := range agents {
390
+ fmt.Printf(" • %s - %s\n", agent.ID, agent.Description)
391
+ }
392
+ fmt.Println("\nUse '/agent <id>' to switch")
393
+ fmt.Println()
394
+ }
395
+ return true, true
396
+
397
+ case "/tools":
398
+ tools := toolRegistry.List()
399
+ fmt.Println("\nAvailable Tools:")
400
+ for _, tool := range tools {
401
+ fmt.Printf(" • %s (%s) - %s\n", tool.Name, tool.RiskLevel, tool.Description)
402
+ }
403
+ fmt.Println()
404
+ return true, true
405
+
406
+ case "/clear":
407
+ agent, _ := agentManager.GetCurrentAgent()
408
+ agentManager.ClearHistory(agent.ID)
409
+ fmt.Println("āœ… Conversation history cleared")
410
+ return true, true
411
+
412
+ case "/auth":
413
+ // Show auth status
414
+ cfg, err := config.LoadConfig()
415
+ if err != nil {
416
+ fmt.Printf("āŒ Error loading config: %v\n", err)
417
+ return true, true
418
+ }
419
+
420
+ fmt.Println("\nšŸ” Authentication Status:")
421
+ fmt.Printf(" Provider: %s\n", cfg.Auth.Provider)
422
+ fmt.Printf(" Auth Type: %s\n", cfg.Auth.Type)
423
+
424
+ if cfg.Auth.APIKey != "" {
425
+ fmt.Printf(" Status: āœ… Authenticated (API Key)\n")
426
+ keyLen := len(cfg.Auth.APIKey)
427
+ if keyLen > 12 {
428
+ fmt.Printf(" Key: %s...%s\n", cfg.Auth.APIKey[:8], cfg.Auth.APIKey[keyLen-4:])
429
+ }
430
+ } else {
431
+ fmt.Printf(" Status: āŒ Not authenticated\n")
432
+ fmt.Println("\n Run: ./dcode auth login --type api_key")
433
+ }
434
+ fmt.Println()
435
+ return true, true
436
+
437
+ case "/exit", "/quit", "/q":
438
+ if f, err := os.Create(historyFile); err == nil {
439
+ linerInst.WriteHistory(f)
440
+ f.Close()
441
+ }
442
+ fmt.Println("\nšŸ‘‹ Goodbye!")
443
+ return true, false
444
+
445
+ default:
446
+ return false, true
447
+ }
448
+ }
package/cmd/analyze.go ADDED
@@ -0,0 +1,97 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "io/fs"
6
+ "os"
7
+ "path/filepath"
8
+ "sort"
9
+ "strings"
10
+
11
+ "github.com/spf13/cobra"
12
+ )
13
+
14
+ var analyzeCmd = &cobra.Command{
15
+ Use: "analyze [path]",
16
+ Short: "Analyze project structure",
17
+ RunE: func(cmd *cobra.Command, args []string) error {
18
+ path := "."
19
+ if len(args) > 0 {
20
+ path = args[0]
21
+ }
22
+ fmt.Printf("Analyzing: %s\n\n", path)
23
+
24
+ var total int
25
+ byExt := map[string]int{}
26
+ markers := map[string]bool{}
27
+ ignoreDirs := map[string]bool{".git": true, "node_modules": true, ".next": true, "dist": true, "build": true, ".venv": true}
28
+
29
+ err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
30
+ if err != nil {
31
+ return err
32
+ }
33
+ name := d.Name()
34
+ if d.IsDir() {
35
+ if ignoreDirs[name] {
36
+ return filepath.SkipDir
37
+ }
38
+ return nil
39
+ }
40
+ total++
41
+ ext := strings.ToLower(filepath.Ext(name))
42
+ if ext == "" {
43
+ ext = "(noext)"
44
+ }
45
+ byExt[ext]++
46
+ base := strings.ToLower(name)
47
+ switch base {
48
+ case "go.mod", "package.json", "pyproject.toml", "requirements.txt", "pom.xml", "build.gradle", "cargo.toml":
49
+ markers[base] = true
50
+ }
51
+ return nil
52
+ })
53
+ if err != nil {
54
+ return err
55
+ }
56
+
57
+ fmt.Printf("Files: %d\n", total)
58
+ if len(markers) > 0 {
59
+ fmt.Println("Detected:")
60
+ keys := make([]string, 0, len(markers))
61
+ for k := range markers {
62
+ keys = append(keys, k)
63
+ }
64
+ sort.Strings(keys)
65
+ for _, k := range keys {
66
+ fmt.Printf(" - %s\n", k)
67
+ }
68
+ fmt.Println()
69
+ }
70
+
71
+ type kv struct {
72
+ k string
73
+ v int
74
+ }
75
+ items := make([]kv, 0, len(byExt))
76
+ for k, v := range byExt {
77
+ items = append(items, kv{k: k, v: v})
78
+ }
79
+ sort.Slice(items, func(i, j int) bool { return items[i].v > items[j].v })
80
+
81
+ fmt.Println("Top file types:")
82
+ for i := 0; i < len(items) && i < 10; i++ {
83
+ fmt.Printf(" %6d %s\n", items[i].v, items[i].k)
84
+ }
85
+ fmt.Println()
86
+
87
+ if _, err := os.Stat(filepath.Join(path, "README.md")); err == nil {
88
+ fmt.Println("Tip: run `dcode edit --file README.md --prompt \"...\"` to update docs.")
89
+ }
90
+ fmt.Println("Analysis complete.")
91
+ return nil
92
+ },
93
+ }
94
+
95
+ func init() {
96
+ rootCmd.AddCommand(analyzeCmd)
97
+ }