diff --git a/cmd/godex/main.go b/cmd/godex/main.go index 3fd03ff..653af60 100644 --- a/cmd/godex/main.go +++ b/cmd/godex/main.go @@ -16,6 +16,7 @@ import ( "os/signal" "path/filepath" "runtime" + "sort" "strconv" "strings" "sync" @@ -406,10 +407,7 @@ func main() { } hiveMgr, err = hive.NewManager(hiveCode, baseDir, provider.Model, maxTokens, getServerNames(servers), statusCh, func(ctx context.Context, prompt string) (string, error) { native := llmProvider != nil && llmProvider.SupportsNativeToolCalls() - toolsDesc := "" - if !native { - toolsDesc = getToolsDescription(servers) - } + toolsDesc := getToolsDescription(servers) fullPrompt := buildFullPrompt(native, toolsDesc, "", wd, prompt, "", hiveCode) maxRounds := 10 if provider.MaxToolRounds != nil && *provider.MaxToolRounds > 0 { @@ -629,10 +627,7 @@ promptLoop: completionMsg := fmt.Sprintf("Hive worker %s has completed and sent back the following result:\n%s", workerLabel, payload) sessionContext := buildSessionContext(prevSession, agentsContext, commitContextPath, commitContextRef, wd) native := llmProvider != nil && llmProvider.SupportsNativeToolCalls() - toolsDesc := "" - if !native { - toolsDesc = getToolsDescription(servers) - } + toolsDesc := getToolsDescription(servers) var hiveInstanceID string if hiveMgr != nil { hiveInstanceID = hiveMgr.Instance().ID @@ -909,12 +904,7 @@ promptLoop: } nativeToolCalls := llmProvider.SupportsNativeToolCalls() - var toolsSection string - if nativeToolCalls { - toolsSection = "" - } else { - toolsSection = fmt.Sprintf("\nYou have access to these tools:\n%s\n", getToolsDescription(servers)) - } + toolsSection := getToolsDescription(servers) sessionContext := buildSessionContext(prevSession, agentsContext, commitContextPath, commitContextRef, wd) @@ -1258,10 +1248,7 @@ promptLoop: completionMsg := fmt.Sprintf("Hive worker %s has completed and sent back the following result:\n%s", workerLabel, payload) sessionContext := buildSessionContext(prevSession, agentsContext, commitContextPath, commitContextRef, wd) native := llmProvider != nil && llmProvider.SupportsNativeToolCalls() - toolsDesc := "" - if !native { - toolsDesc = getToolsDescription(servers) - } + toolsDesc := getToolsDescription(servers) var hiveInstanceID string if hiveMgr != nil { hiveInstanceID = hiveMgr.Instance().ID @@ -2062,10 +2049,7 @@ func runSinglePrompt(ctx context.Context, provider *config.Provider, prompt stri llmProvider, _ := agent.GetProvider(provider) nativeToolCalls := llmProvider != nil && llmProvider.SupportsNativeToolCalls() - toolsDesc := "" - if !nativeToolCalls { - toolsDesc = getToolsDescription(servers) - } + toolsDesc := getToolsDescription(servers) fullPrompt := buildFullPrompt(nativeToolCalls, toolsDesc, "", wd, prompt, tree, "") // Get tool settings @@ -2099,33 +2083,46 @@ func buildFullPrompt(nativeToolCalls bool, toolsSection, sessionContext, wd, inp hiveInfo = fmt.Sprintf(`- Hive Instance ID: %s. You can't assign a task to yourself, but you can ask other hive workers to perform tasks for you. `, hiveInstanceID) } + osName := runtime.GOOS base += fmt.Sprintf(`CRITICAL INFORMATION: - Operating System: %s (%s) - Current working directory: %s %sUse this path when the user asks about "this folder", "current directory", or similar. IMPORTANT: Execute tools FIRST, perform any action asked for by the user, then provide the final answer. Do NOT include any final answer, summary, or "FINAL_ANSWER:" until AFTER you have executed all necessary tools and received their results. If you need to run commands/tests to verify something, run them first before answering. -`, runtime.GOOS, runtime.GOARCH, wd, hiveInfo) +`, osName, runtime.GOARCH, wd, hiveInfo) - if !nativeToolCalls { - base = fmt.Sprintf(`You have access to these tools: -%s + if strings.TrimSpace(toolsSection) != "" { + base += fmt.Sprintf("\nYou have access to these tools:\n%s\n", toolsSection) + } -%s + shellName := "sh" + shellLimits := `- Shell variables like $HOME, $PATH are NOT expanded - use absolute paths instead +- Interactive commands (vim, less, top) will NOT work - use non-interactive alternatives +- Shell aliases are NOT expanded - use full command names` + if osName == "windows" { + shellName = "cmd.exe" + shellLimits = `- Use cmd.exe syntax (e.g., %USERPROFILE% not $HOME, %PATH% not $PATH) +- PowerShell-specific commands may not work; prefer cmd.exe built-ins +- Interactive commands will NOT work - use non-interactive alternatives` + } + base += fmt.Sprintf(` IMPORTANT: When you need to read files, search, or get directory contents, you MUST call the appropriate tool with the CORRECT path. Do NOT use example paths like "/path/to/directory" - use the actual path: %s BASH TOOL LIMITATIONS: -- Shell variables like $HOME, $PATH are NOT expanded - use absolute paths instead -- Interactive commands (vim, less, top) will NOT work - use non-interactive alternatives -- Shell aliases are NOT expanded - use full command names +- Commands run via `+"`"+`%s`+"`"+` +- %s - NEVER run servers or long-running programs in foreground - they will hang. ALWAYS use background: true - ALWAYS use background: true for any server, daemon, or program that doesn't exit immediately - ALWAYS use background: true when starting a webserver or a long-running background task - After starting a background process, use sleep before making requests to it - Use kill_command with the PID to stop background processes when done +`, wd, shellName, shellLimits) + if !nativeToolCalls { + base += ` To call tools, respond with one or more JSON objects, each in its own markdown code block. You can call multiple tools at once to be more efficient. If tools are independent of each other, call them in parallel for faster execution. When planning parallel tool calls, ensure there is no conflict - tools that modify the same resource (file, directory, process, etc.) should NOT be called in parallel; run them sequentially instead. @@ -2140,8 +2137,7 @@ Example: } `+"```"+` -IMPORTANT: Execute tools FIRST, perform any action asked for by the user, then provide the final answer. Do NOT include any final answer, summary, or "FINAL_ANSWER:" until AFTER you have executed all necessary tools and received their results. If you need to run commands/tests to verify something, run them first before answering. -`, toolsSection, base, wd) +IMPORTANT: Execute tools FIRST, perform any action asked for by the user, then provide the final answer. Do NOT include any final answer, summary, or "FINAL_ANSWER:" until AFTER you have executed all necessary tools and received their results. If you need to run commands/tests to verify something, run them first before answering.` } if strings.TrimSpace(tree) != "" { @@ -2154,10 +2150,7 @@ User request: %s`, strings.TrimSpace(base), input) func runToolLoop(ctx context.Context, provider *config.Provider, servers []MCPServer, fullPrompt, input string, maxToolRounds int, toolTimeout int, llmProvider providers.Provider, verbose, debug, autoDenyRestrictedPaths bool, nocont bool, onToolCall func(string)) (string, error) { prevRoundToolCalls := make(map[string]bool) - toolsDesc := "" - if llmProvider == nil || !llmProvider.SupportsNativeToolCalls() { - toolsDesc = getToolsDescription(servers) - } + toolsDesc := getToolsDescription(servers) prevNoTool := false for round := 0; round < maxToolRounds; round++ { @@ -2376,10 +2369,10 @@ func runToolLoop(ctx context.Context, provider *config.Provider, servers []MCPSe } func buildContinuePrompt(llmProvider providers.Provider, servers []MCPServer, originalInput, continuePrompt string, currentRound, maxRounds int) string { - if llmProvider != nil && llmProvider.SupportsNativeToolCalls() { - return fmt.Sprintf("Continue from where you left off. %s\n\nProvide your FINAL_ANSWER now.", continuePrompt) - } toolsDesc := getToolsDescription(servers) + if llmProvider != nil && llmProvider.SupportsNativeToolCalls() { + return fmt.Sprintf("You have access to these tools:%s\n\nContinue from where you left off. %s\n\nProvide your FINAL_ANSWER now.", toolsDesc, continuePrompt) + } toolCallFormat := "To call tools, respond with JSON in markdown code blocks:\n```json\n{\n \"name\": \"tool_name\",\n \"arguments\": {\n \"arg1\": \"value1\"\n }\n}\n```" roundInfo := "" if currentRound > 0 { @@ -2403,6 +2396,40 @@ func getToolsDescription(servers []MCPServer) string { desc.WriteString(fmt.Sprintf("\n[%s]\n", serverName)) for _, tool := range tools { desc.WriteString(fmt.Sprintf(" - %s: %s\n", tool.Name, tool.Description)) + var schema map[string]any + if err := json.Unmarshal(tool.InputSchema, &schema); err != nil { + continue + } + props, _ := schema["properties"].(map[string]any) + required, _ := schema["required"].([]any) + reqSet := make(map[string]bool, len(required)) + for _, r := range required { + if s, ok := r.(string); ok { + reqSet[s] = true + } + } + if len(props) > 0 { + // Sort for deterministic output + names := make([]string, 0, len(props)) + for name := range props { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + p, _ := props[name].(map[string]any) + pType, _ := p["type"].(string) + pDesc, _ := p["description"].(string) + req := "optional" + if reqSet[name] { + req = "required" + } + if pDesc != "" { + desc.WriteString(fmt.Sprintf(" %s (%s, %s): %s\n", name, pType, req, pDesc)) + } else { + desc.WriteString(fmt.Sprintf(" %s (%s, %s)\n", name, pType, req)) + } + } + } } } return desc.String() diff --git a/internal/mcp/bash.go b/internal/mcp/bash.go index 91a0a31..d1bd236 100644 --- a/internal/mcp/bash.go +++ b/internal/mcp/bash.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "sync" "syscall" @@ -271,8 +272,8 @@ func NewBashServer(allowedPaths []string, autoConfirm bool) *BashServer { jobTracker: NewJobTracker(), tools: []Tool{ { - Name: "run_command", - Description: "Run a shell command and return its output. For servers/services/long-running processes, you MUST set background: true (killable via /kill or /killbg).", + Name: "run_command", + Description: "Run a shell command and return its output. Uses cmd.exe on Windows, sh on Unix. For servers/services/long-running processes, you MUST set background: true (killable via /kill or /killbg).", InputSchema: json.RawMessage(`{"type":"object","properties":{"command":{"type":"string","description":"Shell command to run"},"timeout":{"type":"number","description":"Timeout in seconds (default 180)"},"background":{"type":"boolean","description":"Run in background as a goroutine (default false)"}},"required":["command"]}`), }, { @@ -594,7 +595,12 @@ func (s *BashServer) runSyncCommand(ctx context.Context, command string, timeout ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "sh", "-c", command) + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.CommandContext(ctx, "cmd", "/c", command) + } else { + cmd = exec.CommandContext(ctx, "sh", "-c", command) + } cmd.Stdin = nil // Close stdin to prevent interactive prompts from blocking var stdout, stderr bytes.Buffer diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 29e7570..2ae752a 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -663,7 +663,7 @@ func (s *Scheduler) executeWithTools(ctx context.Context, task *ScheduledTask) ( } func (s *Scheduler) buildInitialPrompt(input, toolsDesc string, toolCallFormat string, nativeToolCalls bool) string { - osInfo := "linux" + osInfo := runtime.GOOS if s.os != "" { osInfo = s.os } @@ -692,7 +692,7 @@ func (s *Scheduler) buildContinuePrompt(continuePrompt, toolsDesc string, toolCa if currentRound > 0 { roundInfo = fmt.Sprintf("\n\nNOTE: You are on round %d/%d. Only call more tools if absolutely necessary.", currentRound, maxRounds) } - osInfo := "linux" + osInfo := runtime.GOOS if s.os != "" { osInfo = s.os } diff --git a/internal/wizard/wizard.go b/internal/wizard/wizard.go index 9857b7a..8f40237 100644 --- a/internal/wizard/wizard.go +++ b/internal/wizard/wizard.go @@ -479,6 +479,14 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } } + // Fallback: on Windows consoles that are not in raw / virtual-terminal + // input mode, Bubble Tea delivers Space as a printable rune rather than + // a KeySpace event. Accept that as a toggle so the multi-select works + // cross-platform (notably fixing the MCP picker on Windows). + if msg.Type == tea.KeyRunes && len(msg.Runes) == 1 && msg.Runes[0] == ' ' { + m.selected[m.cursor] = !m.selected[m.cursor] + return m, nil + } } return m, nil }