@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,338 @@
1
+ package prompt
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ "github.com/charmbracelet/bubbles/textinput"
8
+ tea "github.com/charmbracelet/bubbletea"
9
+ "github.com/charmbracelet/lipgloss"
10
+ "github.com/ddhanush1/dcode/internal/ai"
11
+ )
12
+
13
+ type slashCommand struct {
14
+ Name string
15
+ Description string
16
+ Category string
17
+ }
18
+
19
+ var availableCommands = []slashCommand{
20
+ {Name: "/analyze", Description: "Analyze project structure and dependencies", Category: "Project"},
21
+ {Name: "/edit", Description: "AI-powered code editing", Category: "Code"},
22
+ {Name: "/fix", Description: "Detect and fix errors in code", Category: "Code"},
23
+ {Name: "/generate", Description: "Generate code from prompts", Category: "Code"},
24
+ {Name: "/search", Description: "Search the web for information", Category: "Web"},
25
+ {Name: "/providers", Description: "List available AI providers and models", Category: "Config"},
26
+ {Name: "/help", Description: "Show available commands", Category: "Help"},
27
+ {Name: "/exit", Description: "Exit the interactive session", Category: "System"},
28
+ }
29
+
30
+ type model struct {
31
+ textInput textinput.Model
32
+ suggestions []slashCommand
33
+ selectedIndex int
34
+ showSuggestions bool
35
+ quitting bool
36
+ aiClient *ai.Client
37
+ chatHistory []ai.Message
38
+ lastResponse string
39
+ isProcessing bool
40
+ errorMsg string
41
+ }
42
+
43
+ type aiResponseMsg struct {
44
+ response string
45
+ err error
46
+ }
47
+
48
+ func callAI(client *ai.Client, messages []ai.Message) tea.Cmd {
49
+ return func() tea.Msg {
50
+ response, err := client.Chat(messages)
51
+ return aiResponseMsg{response: response, err: err}
52
+ }
53
+ }
54
+
55
+ func initialModel() model {
56
+ ti := textinput.New()
57
+ ti.Placeholder = "Type your prompt or / for commands..."
58
+ ti.Focus()
59
+ ti.CharLimit = 500
60
+ ti.Width = 80
61
+
62
+ // Initialize AI client
63
+ client, err := ai.NewClient()
64
+ if err != nil {
65
+ fmt.Printf("⚠️ Warning: Could not initialize AI client: %v\n", err)
66
+ fmt.Println("Please set your API keys in .env file")
67
+ fmt.Println()
68
+ }
69
+
70
+ return model{
71
+ textInput: ti,
72
+ suggestions: []slashCommand{},
73
+ selectedIndex: 0,
74
+ showSuggestions: false,
75
+ quitting: false,
76
+ aiClient: client,
77
+ chatHistory: []ai.Message{},
78
+ lastResponse: "",
79
+ isProcessing: false,
80
+ errorMsg: "",
81
+ }
82
+ }
83
+
84
+ func (m model) Init() tea.Cmd {
85
+ return textinput.Blink
86
+ }
87
+
88
+ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
89
+ var cmd tea.Cmd
90
+
91
+ switch msg := msg.(type) {
92
+ case aiResponseMsg:
93
+ m.isProcessing = false
94
+ if msg.err != nil {
95
+ m.errorMsg = fmt.Sprintf("Error: %v", msg.err)
96
+ } else {
97
+ m.lastResponse = msg.response
98
+ m.chatHistory = append(m.chatHistory, ai.Message{
99
+ Role: "assistant",
100
+ Content: msg.response,
101
+ })
102
+ m.errorMsg = ""
103
+ }
104
+ return m, nil
105
+
106
+ case tea.KeyMsg:
107
+ switch msg.String() {
108
+ case "ctrl+c":
109
+ m.quitting = true
110
+ return m, tea.Quit
111
+
112
+ case "esc":
113
+ if m.showSuggestions {
114
+ m.showSuggestions = false
115
+ m.suggestions = []slashCommand{}
116
+ } else {
117
+ m.quitting = true
118
+ return m, tea.Quit
119
+ }
120
+ return m, nil
121
+
122
+ case "enter":
123
+ if m.showSuggestions && len(m.suggestions) > 0 {
124
+ // Select the highlighted suggestion
125
+ selected := m.suggestions[m.selectedIndex]
126
+ m.textInput.SetValue(selected.Name + " ")
127
+ m.textInput.CursorEnd()
128
+ m.showSuggestions = false
129
+ m.suggestions = []slashCommand{}
130
+ } else {
131
+ // Process the input
132
+ value := m.textInput.Value()
133
+ if value != "" {
134
+ if value == "/exit" {
135
+ m.quitting = true
136
+ return m, tea.Quit
137
+ }
138
+
139
+ // Handle slash commands
140
+ if strings.HasPrefix(value, "/") {
141
+ m.lastResponse = fmt.Sprintf("Command '%s' not yet implemented", value)
142
+ m.textInput.SetValue("")
143
+ return m, nil
144
+ }
145
+
146
+ // Send to AI
147
+ if m.aiClient != nil && !m.isProcessing {
148
+ m.isProcessing = true
149
+ m.errorMsg = ""
150
+ m.lastResponse = ""
151
+
152
+ // Add user message to history
153
+ m.chatHistory = append(m.chatHistory, ai.Message{
154
+ Role: "user",
155
+ Content: value,
156
+ })
157
+
158
+ m.textInput.SetValue("")
159
+ return m, callAI(m.aiClient, m.chatHistory)
160
+ } else if m.aiClient == nil {
161
+ m.errorMsg = "AI client not initialized. Please check your API keys."
162
+ m.textInput.SetValue("")
163
+ }
164
+ }
165
+ }
166
+ return m, nil
167
+
168
+ case "tab":
169
+ if m.showSuggestions && len(m.suggestions) > 0 {
170
+ selected := m.suggestions[m.selectedIndex]
171
+ m.textInput.SetValue(selected.Name + " ")
172
+ m.textInput.CursorEnd()
173
+ m.showSuggestions = false
174
+ m.suggestions = []slashCommand{}
175
+ }
176
+ return m, nil
177
+
178
+ case "up":
179
+ if m.showSuggestions && len(m.suggestions) > 0 {
180
+ if m.selectedIndex > 0 {
181
+ m.selectedIndex--
182
+ }
183
+ }
184
+ return m, nil
185
+
186
+ case "down":
187
+ if m.showSuggestions && len(m.suggestions) > 0 {
188
+ if m.selectedIndex < len(m.suggestions)-1 {
189
+ m.selectedIndex++
190
+ }
191
+ }
192
+ return m, nil
193
+ }
194
+ }
195
+
196
+ m.textInput, cmd = m.textInput.Update(msg)
197
+
198
+ // Check if we should show suggestions
199
+ value := m.textInput.Value()
200
+ if strings.HasPrefix(value, "/") && len(value) > 0 {
201
+ filtered := filterCommands(value)
202
+ if len(filtered) > 0 {
203
+ m.showSuggestions = true
204
+ m.suggestions = filtered
205
+ m.selectedIndex = 0
206
+ } else {
207
+ m.showSuggestions = false
208
+ m.suggestions = []slashCommand{}
209
+ }
210
+ } else {
211
+ m.showSuggestions = false
212
+ m.suggestions = []slashCommand{}
213
+ }
214
+
215
+ return m, cmd
216
+ }
217
+
218
+ func (m model) View() string {
219
+ if m.quitting {
220
+ return "Goodbye! 👋\n"
221
+ }
222
+
223
+ var b strings.Builder
224
+
225
+ // Title with provider info
226
+ titleStyle := lipgloss.NewStyle().
227
+ Bold(true).
228
+ Foreground(lipgloss.Color("#7D56F4")).
229
+ MarginBottom(1)
230
+
231
+ providerInfo := ""
232
+ if m.aiClient != nil {
233
+ providerInfo = fmt.Sprintf(" (%s - %s)", m.aiClient.Provider, m.aiClient.Model)
234
+ }
235
+ b.WriteString(titleStyle.Render("🤖 dcode-cli - AI Coding Assistant" + providerInfo))
236
+ b.WriteString("\n\n")
237
+
238
+ // Show last response if available
239
+ if m.lastResponse != "" {
240
+ responseStyle := lipgloss.NewStyle().
241
+ Foreground(lipgloss.Color("#FAFAFA")).
242
+ Background(lipgloss.Color("#1e1e1e")).
243
+ Padding(1, 2).
244
+ MarginBottom(1).
245
+ Width(80)
246
+
247
+ b.WriteString(responseStyle.Render("🤖 AI: " + m.lastResponse))
248
+ b.WriteString("\n\n")
249
+ }
250
+
251
+ // Show error if any
252
+ if m.errorMsg != "" {
253
+ errorStyle := lipgloss.NewStyle().
254
+ Foreground(lipgloss.Color("#FF0000")).
255
+ Bold(true)
256
+ b.WriteString(errorStyle.Render("⚠️ " + m.errorMsg))
257
+ b.WriteString("\n\n")
258
+ }
259
+
260
+ // Show processing indicator
261
+ if m.isProcessing {
262
+ processingStyle := lipgloss.NewStyle().
263
+ Foreground(lipgloss.Color("#FFA500")).
264
+ Italic(true)
265
+ b.WriteString(processingStyle.Render("⏳ Processing..."))
266
+ b.WriteString("\n\n")
267
+ }
268
+
269
+ // Input box
270
+ b.WriteString(m.textInput.View())
271
+ b.WriteString("\n")
272
+
273
+ // Show suggestions if slash command is typed
274
+ if m.showSuggestions && len(m.suggestions) > 0 {
275
+ b.WriteString("\n")
276
+
277
+ suggestionStyle := lipgloss.NewStyle().
278
+ Border(lipgloss.RoundedBorder()).
279
+ BorderForeground(lipgloss.Color("#874BFD")).
280
+ Padding(1, 2)
281
+
282
+ selectedStyle := lipgloss.NewStyle().
283
+ Foreground(lipgloss.Color("#FAFAFA")).
284
+ Background(lipgloss.Color("#7D56F4")).
285
+ Bold(true)
286
+
287
+ normalStyle := lipgloss.NewStyle().
288
+ Foreground(lipgloss.Color("#FAFAFA"))
289
+
290
+ descStyle := lipgloss.NewStyle().
291
+ Foreground(lipgloss.Color("#626262")).
292
+ Italic(true)
293
+
294
+ var suggestions strings.Builder
295
+ suggestions.WriteString("Available Commands:\n\n")
296
+
297
+ for i, cmd := range m.suggestions {
298
+ style := normalStyle
299
+ if i == m.selectedIndex {
300
+ style = selectedStyle
301
+ }
302
+
303
+ suggestions.WriteString(style.Render(fmt.Sprintf("%-15s", cmd.Name)))
304
+ suggestions.WriteString(" ")
305
+ suggestions.WriteString(descStyle.Render(cmd.Description))
306
+ suggestions.WriteString("\n")
307
+ }
308
+
309
+ b.WriteString(suggestionStyle.Render(suggestions.String()))
310
+ }
311
+
312
+ // Help text
313
+ b.WriteString("\n\n")
314
+ helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#626262"))
315
+ b.WriteString(helpStyle.Render("Press / for commands • Tab/Enter to select • ↑↓ to navigate • Ctrl+C to exit"))
316
+
317
+ return b.String()
318
+ }
319
+
320
+ func filterCommands(input string) []slashCommand {
321
+ input = strings.ToLower(input)
322
+ var filtered []slashCommand
323
+
324
+ for _, cmd := range availableCommands {
325
+ if strings.HasPrefix(strings.ToLower(cmd.Name), input) {
326
+ filtered = append(filtered, cmd)
327
+ }
328
+ }
329
+
330
+ return filtered
331
+ }
332
+
333
+ // StartInteractive starts the interactive prompt interface
334
+ func StartInteractive() error {
335
+ p := tea.NewProgram(initialModel())
336
+ _, err := p.Run()
337
+ return err
338
+ }
@@ -0,0 +1,201 @@
1
+ package registry
2
+
3
+ import (
4
+ "fmt"
5
+ "sync"
6
+ )
7
+
8
+ // AgentDefinition defines metadata and capabilities of an agent
9
+ type AgentDefinition struct {
10
+ // Unique identifier for the agent
11
+ ID string `json:"id"`
12
+
13
+ // Human-readable name
14
+ Name string `json:"name"`
15
+
16
+ // Description of what the agent does
17
+ Description string `json:"description"`
18
+
19
+ // Version of the agent
20
+ Version string `json:"version"`
21
+
22
+ // Tools this agent can use
23
+ Tools []string `json:"tools"`
24
+
25
+ // System prompt for the agent
26
+ SystemPrompt string `json:"system_prompt"`
27
+
28
+ // Whether this agent can invoke sub-agents
29
+ AllowSubAgents bool `json:"allow_sub_agents"`
30
+
31
+ // Maximum conversation turns
32
+ MaxTurns int `json:"max_turns"`
33
+
34
+ // Agent-specific configuration
35
+ Config map[string]interface{} `json:"config"`
36
+
37
+ // Executor function
38
+ Executor AgentExecutor `json:"-"`
39
+
40
+ // Instance - optional reference to actual agent instance (for streaming, etc.)
41
+ Instance interface{} `json:"-"`
42
+ }
43
+
44
+ // AgentExecutor is a function that executes an agent
45
+ type AgentExecutor func(input *AgentInput) (*AgentOutput, error)
46
+
47
+ // AgentInput represents input to an agent
48
+ type AgentInput struct {
49
+ // User query or instruction
50
+ Query string
51
+
52
+ // Structured input parameters
53
+ Parameters map[string]interface{}
54
+
55
+ // Conversation history
56
+ History []Message
57
+
58
+ // Available tools
59
+ Tools []string
60
+
61
+ // Tool registry for function calling
62
+ ToolRegistry *ToolRegistry
63
+
64
+ // System prompt override
65
+ SystemPrompt string
66
+
67
+ // Context information
68
+ Context map[string]interface{}
69
+
70
+ // Agent configuration
71
+ Config map[string]interface{}
72
+ }
73
+
74
+ // AgentOutput represents output from an agent
75
+ type AgentOutput struct {
76
+ // Response message
77
+ Message string
78
+
79
+ // Tool calls made by the agent
80
+ ToolCalls []ToolCall
81
+
82
+ // Structured output
83
+ Data map[string]interface{}
84
+
85
+ // Termination reason
86
+ TerminationReason string
87
+
88
+ // Metadata
89
+ Metadata map[string]interface{}
90
+ }
91
+
92
+ // Message represents a conversation message
93
+ type Message struct {
94
+ Role string `json:"role"`
95
+ Content string `json:"content"`
96
+ Data map[string]interface{} `json:"data,omitempty"`
97
+ }
98
+
99
+ // ToolCall represents a tool invocation
100
+ type ToolCall struct {
101
+ ID string `json:"id"`
102
+ ToolName string `json:"tool_name"`
103
+ Parameters map[string]interface{} `json:"parameters"` // Changed from Arguments
104
+ Result interface{} `json:"result,omitempty"`
105
+ Error string `json:"error,omitempty"`
106
+ Status string `json:"status,omitempty"` // Added status field
107
+ }
108
+
109
+ // AgentRegistry manages agent registration and discovery
110
+ type AgentRegistry struct {
111
+ agents map[string]*AgentDefinition
112
+ mu sync.RWMutex
113
+ }
114
+
115
+ // NewAgentRegistry creates a new agent registry
116
+ func NewAgentRegistry() *AgentRegistry {
117
+ return &AgentRegistry{
118
+ agents: make(map[string]*AgentDefinition),
119
+ }
120
+ }
121
+
122
+ // Register registers an agent
123
+ func (r *AgentRegistry) Register(agent *AgentDefinition) error {
124
+ r.mu.Lock()
125
+ defer r.mu.Unlock()
126
+
127
+ if agent.ID == "" {
128
+ return fmt.Errorf("agent ID is required")
129
+ }
130
+
131
+ if agent.Name == "" {
132
+ return fmt.Errorf("agent name is required")
133
+ }
134
+
135
+ if agent.Executor == nil {
136
+ return fmt.Errorf("agent executor is required")
137
+ }
138
+
139
+ r.agents[agent.ID] = agent
140
+ return nil
141
+ }
142
+
143
+ // Get retrieves an agent by ID
144
+ func (r *AgentRegistry) Get(id string) (*AgentDefinition, error) {
145
+ r.mu.RLock()
146
+ defer r.mu.RUnlock()
147
+
148
+ agent, ok := r.agents[id]
149
+ if !ok {
150
+ return nil, fmt.Errorf("agent not found: %s", id)
151
+ }
152
+
153
+ return agent, nil
154
+ }
155
+
156
+ // List returns all registered agents
157
+ func (r *AgentRegistry) List() []*AgentDefinition {
158
+ r.mu.RLock()
159
+ defer r.mu.RUnlock()
160
+
161
+ agents := make([]*AgentDefinition, 0, len(r.agents))
162
+ for _, agent := range r.agents {
163
+ agents = append(agents, agent)
164
+ }
165
+
166
+ return agents
167
+ }
168
+
169
+ // Unregister removes an agent from the registry
170
+ func (r *AgentRegistry) Unregister(id string) error {
171
+ r.mu.Lock()
172
+ defer r.mu.Unlock()
173
+
174
+ if _, ok := r.agents[id]; !ok {
175
+ return fmt.Errorf("agent not found: %s", id)
176
+ }
177
+
178
+ delete(r.agents, id)
179
+ return nil
180
+ }
181
+
182
+ // Has checks if an agent exists
183
+ func (r *AgentRegistry) Has(id string) bool {
184
+ r.mu.RLock()
185
+ defer r.mu.RUnlock()
186
+
187
+ _, ok := r.agents[id]
188
+ return ok
189
+ }
190
+
191
+ // Global registry instance
192
+ var globalRegistry *AgentRegistry
193
+ var globalRegistryOnce sync.Once
194
+
195
+ // Global returns the global agent registry
196
+ func Global() *AgentRegistry {
197
+ globalRegistryOnce.Do(func() {
198
+ globalRegistry = NewAgentRegistry()
199
+ })
200
+ return globalRegistry
201
+ }
@@ -0,0 +1,181 @@
1
+ package registry
2
+
3
+ import (
4
+ "fmt"
5
+ "sync"
6
+ )
7
+
8
+ // ToolDefinition defines metadata and capabilities of a tool
9
+ type ToolDefinition struct {
10
+ // Unique identifier for the tool
11
+ ID string `json:"id"`
12
+
13
+ // Human-readable name
14
+ Name string `json:"name"`
15
+
16
+ // Description of what the tool does
17
+ Description string `json:"description"`
18
+
19
+ // Input schema (JSON schema)
20
+ InputSchema map[string]interface{} `json:"input_schema"`
21
+
22
+ // Output schema (JSON schema)
23
+ OutputSchema map[string]interface{} `json:"output_schema"`
24
+
25
+ // Whether this tool requires confirmation
26
+ RequiresConfirmation bool `json:"requires_confirmation"`
27
+
28
+ // Risk level: "low", "medium", "high"
29
+ RiskLevel string `json:"risk_level"`
30
+
31
+ // Tool category: "file", "shell", "web", "git", etc.
32
+ Category string `json:"category"`
33
+
34
+ // Tool-specific configuration
35
+ Config map[string]interface{} `json:"config"`
36
+
37
+ // Executor function
38
+ Executor ToolExecutor `json:"-"`
39
+ }
40
+
41
+ // ToolExecutor is a function that executes a tool
42
+ type ToolExecutor func(input *ToolInput) (*ToolOutput, error)
43
+
44
+ // ToolInput represents input to a tool
45
+ type ToolInput struct {
46
+ // Tool name
47
+ ToolName string
48
+
49
+ // Input parameters
50
+ Parameters map[string]interface{}
51
+
52
+ // Context information
53
+ Context map[string]interface{}
54
+
55
+ // Tool configuration
56
+ Config map[string]interface{}
57
+ }
58
+
59
+ // ToolOutput represents output from a tool
60
+ type ToolOutput struct {
61
+ // Success status
62
+ Success bool
63
+
64
+ // Output data
65
+ Data interface{}
66
+
67
+ // Error message if any
68
+ Error string
69
+
70
+ // Metadata
71
+ Metadata map[string]interface{}
72
+ }
73
+
74
+ // ToolRegistry manages tool registration and discovery
75
+ type ToolRegistry struct {
76
+ tools map[string]*ToolDefinition
77
+ mu sync.RWMutex
78
+ }
79
+
80
+ // NewToolRegistry creates a new tool registry
81
+ func NewToolRegistry() *ToolRegistry {
82
+ return &ToolRegistry{
83
+ tools: make(map[string]*ToolDefinition),
84
+ }
85
+ }
86
+
87
+ // Register registers a tool
88
+ func (r *ToolRegistry) Register(tool *ToolDefinition) error {
89
+ r.mu.Lock()
90
+ defer r.mu.Unlock()
91
+
92
+ if tool.ID == "" {
93
+ return fmt.Errorf("tool ID is required")
94
+ }
95
+
96
+ if tool.Name == "" {
97
+ return fmt.Errorf("tool name is required")
98
+ }
99
+
100
+ if tool.Executor == nil {
101
+ return fmt.Errorf("tool executor is required")
102
+ }
103
+
104
+ r.tools[tool.ID] = tool
105
+ return nil
106
+ }
107
+
108
+ // Get retrieves a tool by ID
109
+ func (r *ToolRegistry) Get(id string) (*ToolDefinition, error) {
110
+ r.mu.RLock()
111
+ defer r.mu.RUnlock()
112
+
113
+ tool, ok := r.tools[id]
114
+ if !ok {
115
+ return nil, fmt.Errorf("tool not found: %s", id)
116
+ }
117
+
118
+ return tool, nil
119
+ }
120
+
121
+ // List returns all registered tools
122
+ func (r *ToolRegistry) List() []*ToolDefinition {
123
+ r.mu.RLock()
124
+ defer r.mu.RUnlock()
125
+
126
+ tools := make([]*ToolDefinition, 0, len(r.tools))
127
+ for _, tool := range r.tools {
128
+ tools = append(tools, tool)
129
+ }
130
+
131
+ return tools
132
+ }
133
+
134
+ // ListByCategory returns tools in a specific category
135
+ func (r *ToolRegistry) ListByCategory(category string) []*ToolDefinition {
136
+ r.mu.RLock()
137
+ defer r.mu.RUnlock()
138
+
139
+ tools := make([]*ToolDefinition, 0)
140
+ for _, tool := range r.tools {
141
+ if tool.Category == category {
142
+ tools = append(tools, tool)
143
+ }
144
+ }
145
+
146
+ return tools
147
+ }
148
+
149
+ // Unregister removes a tool from the registry
150
+ func (r *ToolRegistry) Unregister(id string) error {
151
+ r.mu.Lock()
152
+ defer r.mu.Unlock()
153
+
154
+ if _, ok := r.tools[id]; !ok {
155
+ return fmt.Errorf("tool not found: %s", id)
156
+ }
157
+
158
+ delete(r.tools, id)
159
+ return nil
160
+ }
161
+
162
+ // Has checks if a tool exists
163
+ func (r *ToolRegistry) Has(id string) bool {
164
+ r.mu.RLock()
165
+ defer r.mu.RUnlock()
166
+
167
+ _, ok := r.tools[id]
168
+ return ok
169
+ }
170
+
171
+ // Global tool registry instance
172
+ var globalToolRegistry *ToolRegistry
173
+ var globalToolRegistryOnce sync.Once
174
+
175
+ // GlobalTools returns the global tool registry
176
+ func GlobalTools() *ToolRegistry {
177
+ globalToolRegistryOnce.Do(func() {
178
+ globalToolRegistry = NewToolRegistry()
179
+ })
180
+ return globalToolRegistry
181
+ }