Merge pull request #20 from cheikh2shift/staticcheck

Fix staticcheck issues and enhance command retrieval
This commit is contained in:
Cheikh Seck authored and GitHub committed 2026-06-06 15:32:54 +00:00
commit b4b9d90258
12 files changed
+217 -147

No files matched your search

+7 -4
View File
@@ -40,8 +40,6 @@ import (
"github.com/cheikh2shift/godex/modelquery"
)
var clipboardAvailable = true
const (
defaultConfigFile = "providers.yaml"
sessionFileName = "session.txt"
@@ -2522,7 +2520,7 @@ func callTool(ctx context.Context, servers []MCPServer, name string, args map[st
allowedPaths := server.AllowedPaths()
if autoDenyRestrictedPaths {
log.Printf("[Hive] Auto-denied restricted path access: %s (allowed paths: %v)\n", path, allowedPaths)
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction.", path)
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction", path)
}
pathPromptMu.Lock()
selected := showPathRestrictionPrompt(path, allowedPaths)
@@ -2546,7 +2544,7 @@ func callTool(ctx context.Context, servers []MCPServer, name string, args map[st
case 2:
return "", ErrUserAborted
case 3:
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction.", path)
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction", path)
}
return "", err
}
@@ -3191,6 +3189,11 @@ func parseToolArgs(data map[string]any) map[string]any {
func shouldExecuteToolCall(text string) ([]map[string]any, bool) {
toolCalls := extractAllToolCalls(text)
if len(toolCalls) == 0 {
if strings.Contains(text, "<tool_code>") {
return []map[string]any{{}}, true
}
return nil, false
}
return toolCalls, true
+1 -3
View File
@@ -660,9 +660,7 @@ func (m *promptModel) handleScheduleTab(value string) bool {
return true
}
m.scheduleList = nil
for _, id := range matches {
m.scheduleList = append(m.scheduleList, id)
}
m.scheduleList = append(m.scheduleList, matches...)
m.resetCompletion()
return true
}
+19 -2
View File
@@ -333,6 +333,8 @@ func (s *BashServer) isPathAllowed(path string) bool {
if path == "" {
return true
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, allowed := range s.allowedPaths {
if strings.HasPrefix(path, allowed) || path == allowed {
return true
@@ -344,17 +346,24 @@ func (s *BashServer) isPathAllowed(path string) bool {
func (s *BashServer) runCommand(ctx context.Context, args map[string]any) (string, error) {
command, ok := args["command"].(string)
if !ok {
return "", fmt.Errorf("command is required")
command, ok = args["cmd"].(string)
if !ok {
return "", fmt.Errorf("command is required")
}
}
if !s.isCommandAllowed(command) {
if interpreterRegex.MatchString(command) {
return "", fmt.Errorf("INTERPRETER_BLOCKED: scripting interpreters (python, ruby, perl, etc.) are not allowed in run_command. Use run_python or run_node instead.")
return "", fmt.Errorf("INTERPRETER_BLOCKED: scripting interpreters (python, ruby, perl, etc.) are not allowed in run_command. Use run_python or run_node instead")
}
allowed, restrictedPath := s.checkCommandPaths(command)
if !allowed && restrictedPath != "" {
if s.autoConfirm {
s.mu.Lock()
s.allowedPaths = append(s.allowedPaths, restrictedPath)
s.mu.Unlock()
log.Printf("[BASH] Auto-confirmed restricted path: %s", restrictedPath)
} else {
return "", fmt.Errorf("PATH_RESTRICTED: path not allowed: %s", restrictedPath)
@@ -811,6 +820,8 @@ func (s *BashServer) AddPath(ctx context.Context, path string) error {
if path == "" {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
for _, p := range s.allowedPaths {
if p == path {
return nil
@@ -825,6 +836,8 @@ func (s *BashServer) TempAddPath(path string) {
if path == "" {
return
}
s.mu.Lock()
defer s.mu.Unlock()
for _, p := range s.allowedPaths {
if p == path {
return
@@ -839,6 +852,8 @@ func (s *BashServer) AddURL(ctx context.Context, url string) error {
func (s *BashServer) RemovePath(ctx context.Context, path string) error {
path = strings.TrimSpace(path)
s.mu.Lock()
defer s.mu.Unlock()
for i, p := range s.allowedPaths {
if p == path {
s.allowedPaths = append(s.allowedPaths[:i], s.allowedPaths[i+1:]...)
@@ -863,7 +878,9 @@ func (s *BashServer) Close() error {
}
func (s *BashServer) SetAllowedPaths(paths []string) {
s.mu.Lock()
s.allowedPaths = sanitizeAllowedPaths(paths)
s.mu.Unlock()
}
func (s *BashServer) AllowedPathsUpdated() {}
+169 -75
View File
@@ -35,18 +35,20 @@ type MCPToolExecutor struct { //betteralign:ignore
serverName string
workingDir string
server MCPServer
server atomic.Pointer[MCPServer]
tools []Tool
mu sync.RWMutex
requestIDMu sync.Mutex
}
func NewMCPServer(ctx context.Context, server MCPServer, workingDir string) (*MCPToolExecutor, error) {
executor := &MCPToolExecutor{
serverName: server.Name,
server: server,
workingDir: workingDir,
}
serverCopy := server
executor.server.Store(&serverCopy)
if err := executor.connect(ctx); err != nil {
return nil, err
@@ -56,11 +58,26 @@ func NewMCPServer(ctx context.Context, server MCPServer, workingDir string) (*MC
}
func (m *MCPToolExecutor) connect(ctx context.Context) error {
args := m.buildArgs()
mcpClient, err := client.NewStdioMCPClient(m.server.Command, m.server.Env, args...)
serverPtr := m.server.Load()
if serverPtr == nil {
return fmt.Errorf("server not initialized")
}
server := *serverPtr
m.mu.Lock()
defer m.mu.Unlock()
mcpClient, tools, err := connect(ctx, server, buildArgs(server, m.workingDir))
if err != nil {
return fmt.Errorf("failed to create MCP client for %s: %w", m.server.Name, err)
return err
}
m.mcpClient = mcpClient
m.tools = tools
return nil
}
func connect(ctx context.Context, server MCPServer, args []string) (*client.Client, []Tool, error) {
mcpClient, err := client.NewStdioMCPClient(server.Command, server.Env, args...)
if err != nil {
return nil, nil, fmt.Errorf("failed to create MCP client for %s: %w", server.Name, err)
}
_, err = mcpClient.Initialize(ctx, mcp.InitializeRequest{
@@ -74,65 +91,80 @@ func (m *MCPToolExecutor) connect(ctx context.Context) error {
})
if err != nil {
_ = mcpClient.Close()
return fmt.Errorf("failed to initialize MCP client for %s: %w", m.server.Name, err)
return nil, nil, fmt.Errorf("failed to initialize MCP client for %s: %w", server.Name, err)
}
tools, err := listTools(ctx, mcpClient)
if err != nil {
_ = mcpClient.Close()
return fmt.Errorf("failed to list tools for %s: %w", m.server.Name, err)
return nil, nil, fmt.Errorf("failed to list tools for %s: %w", server.Name, err)
}
m.mcpClient = mcpClient
m.tools = tools
return nil
return mcpClient, tools, nil
}
// ping checks if the MCP client is still responsive
func (m *MCPToolExecutor) ping(ctx context.Context) error {
if m.mcpClient == nil {
return fmt.Errorf("client is nil")
}
// pingClient checks if the MCP client is still responsive
func pingClient(ctx context.Context, client client.MCPClient) error {
// Create a context with 2 second deadline
pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return m.mcpClient.Ping(pingCtx)
return client.Ping(pingCtx)
}
// ensureConnected pings the client and reconnects if needed
func (m *MCPToolExecutor) ensureConnected(ctx context.Context) error {
if m.mcpClient == nil {
return m.connect(ctx)
}
if err := m.ping(ctx); err != nil {
// Ping failed, close and reconnect
_ = m.mcpClient.Close()
m.mcpClient = nil
if err := m.connect(ctx); err != nil {
return fmt.Errorf("failed to reconnect after ping failure: %w", err)
m.mu.Lock()
client := m.mcpClient
wd := m.workingDir
if client != nil {
if pingClient(ctx, client) == nil {
m.mu.Unlock()
return nil
}
// Ping failed, close and reconnect
_ = client.Close()
m.mcpClient = nil
}
// Do not hold the lock till connecting
m.mu.Unlock()
serverPtr := m.server.Load()
if serverPtr == nil {
return fmt.Errorf("server not initialized")
}
server := *serverPtr
client, tools, err := connect(ctx, server, buildArgs(server, wd))
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
// Guard against another goroutine having reconnected while we were unlocked
if m.mcpClient != nil { // assume it's ok
_ = client.Close()
return nil
}
m.mcpClient, m.tools = client, tools
return nil
}
func (m *MCPToolExecutor) buildArgs() []string {
paths := m.server.AllowedPaths
func buildArgs(server MCPServer, workingDir string) []string {
paths := server.AllowedPaths
if len(paths) == 0 {
paths = []string{m.workingDir}
paths = []string{workingDir}
}
if m.server.Name == "filesystem" || contains(m.server.Args, "server-filesystem") {
if server.Name == "filesystem" || contains(server.Args, "server-filesystem") {
args := []string{"-y", "@modelcontextprotocol/server-filesystem"}
args = append(args, paths...)
return args
}
if len(m.server.Args) > 0 {
return m.server.Args
if len(server.Args) > 0 {
return server.Args
}
return nil
@@ -158,25 +190,45 @@ func listTools(ctx context.Context, mcpClient *client.Client) ([]Tool, error) {
}
func (m *MCPToolExecutor) Name() string {
return m.server.Name
serverPtr := m.server.Load()
if serverPtr == nil {
return ""
}
return serverPtr.Name
}
func (m *MCPToolExecutor) Tools() []Tool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.tools
}
func (m *MCPToolExecutor) AddPath(ctx context.Context, path string) error {
for _, p := range m.server.AllowedPaths {
if p == path {
return nil
serverPtr := m.server.Load()
if serverPtr == nil {
return fmt.Errorf("server not initialized")
}
server := *serverPtr
if func() bool {
for _, p := range server.AllowedPaths {
if p == path {
return true
}
}
server.AllowedPaths = append(server.AllowedPaths, path)
m.server.Store(&server)
m.mu.Lock()
defer m.mu.Unlock()
_ = m.mcpClient.Close()
m.mcpClient = nil
return false
}() {
return nil
}
m.server.AllowedPaths = append(m.server.AllowedPaths, path)
_ = m.mcpClient.Close()
if err := m.connect(ctx); err != nil {
if err := m.ensureConnected(ctx); err != nil {
return fmt.Errorf("failed to reconnect with new path: %w", err)
}
@@ -188,33 +240,53 @@ func (m *MCPToolExecutor) AddURL(ctx context.Context, url string) error {
}
func (m *MCPToolExecutor) TempAddPath(path string) {
for _, p := range m.server.AllowedPaths {
serverPtr := m.server.Load()
if serverPtr == nil {
return
}
server := *serverPtr
for _, p := range server.AllowedPaths {
if p == path {
return
}
}
m.server.AllowedPaths = append(m.server.AllowedPaths, path)
server.AllowedPaths = append(server.AllowedPaths, path)
m.server.Store(&server)
}
func (m *MCPToolExecutor) RemovePath(ctx context.Context, path string) error {
found := -1
for i, p := range m.server.AllowedPaths {
if p == path {
found = i
break
if func() bool {
serverPtr := m.server.Load()
if serverPtr == nil {
return true
}
server := *serverPtr
found := -1
for i, p := range server.AllowedPaths {
if p == path {
found = i
break
}
}
}
if found == -1 {
if found == -1 {
return true
}
server.AllowedPaths = append(server.AllowedPaths[:found], server.AllowedPaths[found+1:]...)
m.server.Store(&server)
m.mu.Lock()
defer m.mu.Unlock()
_ = m.mcpClient.Close()
m.mcpClient = nil
return false
}() {
return nil
}
m.server.AllowedPaths = append(m.server.AllowedPaths[:found], m.server.AllowedPaths[found+1:]...)
_ = m.mcpClient.Close()
if err := m.connect(ctx); err != nil {
if err := m.ensureConnected(ctx); err != nil {
return fmt.Errorf("failed to reconnect after removing path: %w", err)
}
@@ -226,11 +298,16 @@ func (m *MCPToolExecutor) RemoveURL(ctx context.Context, url string) error {
}
func (m *MCPToolExecutor) AllowedPaths() []string {
paths := m.server.AllowedPaths
if len(paths) == 0 {
return []string{m.workingDir}
serverPtr := m.server.Load()
if serverPtr != nil {
paths := serverPtr.AllowedPaths
if len(paths) != 0 {
return paths
}
}
return paths
m.mu.RLock()
defer m.mu.RUnlock()
return []string{m.workingDir}
}
func (m *MCPToolExecutor) CallTool(ctx context.Context, name string, arguments map[string]any) (string, error) {
@@ -257,8 +334,10 @@ func (m *MCPToolExecutor) callToolWithRetry(ctx context.Context, name string, ar
}
if isRetryableError(err.Error()) && attempt < maxRetries {
m.mu.Lock()
_ = m.mcpClient.Close()
m.mcpClient = nil
m.mu.Unlock()
if reconnectErr := m.connect(ctx); reconnectErr == nil {
return m.callToolWithRetry(ctx, name, arguments, attempt+1)
}
@@ -270,7 +349,15 @@ func (m *MCPToolExecutor) callToolWithRetry(ctx context.Context, name string, ar
}
func (m *MCPToolExecutor) callToolWithCancellation(ctx context.Context, requestID int64, name string, arguments map[string]any) (string, error) {
t := m.mcpClient.GetTransport()
var t transport.Interface
m.mu.RLock()
if client := m.mcpClient; client != nil {
t = client.GetTransport()
}
m.mu.RUnlock()
if t == nil {
return "", fmt.Errorf("nil client")
}
request := transport.JSONRPCRequest{
JSONRPC: mcp.JSONRPC_VERSION,
@@ -296,7 +383,7 @@ func (m *MCPToolExecutor) callToolWithCancellation(ctx context.Context, requestI
select {
case <-ctx.Done():
m.sendCancelledNotification(context.Background(), requestID, ctx.Err().Error())
sendCancelledNotification(context.Background(), t, requestID, ctx.Err().Error())
return "", ctx.Err()
case response := <-resultCh:
return m.parseToolResponse(response)
@@ -305,9 +392,7 @@ func (m *MCPToolExecutor) callToolWithCancellation(ctx context.Context, requestI
}
}
func (m *MCPToolExecutor) sendCancelledNotification(ctx context.Context, requestID int64, reason string) {
t := m.mcpClient.GetTransport()
func sendCancelledNotification(ctx context.Context, t transport.Interface, requestID int64, reason string) {
notification := mcp.JSONRPCNotification{
JSONRPC: mcp.JSONRPC_VERSION,
Notification: mcp.Notification{
@@ -369,16 +454,20 @@ func isRetryableError(errMsg string) bool {
}
func (m *MCPToolExecutor) Close() error {
if m.mcpClient != nil {
return m.closeWithTimeout(2 * time.Second)
m.mu.Lock()
client := m.mcpClient
m.mcpClient = nil
m.mu.Unlock()
if client != nil {
return closeWithTimeout(client, 2*time.Second)
}
return nil
}
func (m *MCPToolExecutor) closeWithTimeout(timeout time.Duration) error {
func closeWithTimeout(client *client.Client, timeout time.Duration) error {
done := make(chan error, 1)
go func() {
done <- m.mcpClient.Close()
done <- client.Close()
}()
select {
@@ -399,10 +488,15 @@ func contains(slice []string, val string) bool {
}
func (m *MCPToolExecutor) GetContext() string {
serverPtr := m.server.Load()
if serverPtr == nil {
return ""
}
server := *serverPtr
var b strings.Builder
b.WriteString("MCP Server: " + m.server.Name + "\n")
b.WriteString("Command: " + m.server.Command + "\n")
b.WriteString("Args: " + strings.Join(m.server.Args, " ") + "\n")
b.WriteString("Allowed Paths: " + strings.Join(m.server.AllowedPaths, ", ") + "\n")
b.WriteString("MCP Server: " + server.Name + "\n")
b.WriteString("Command: " + server.Command + "\n")
b.WriteString("Args: " + strings.Join(server.Args, " ") + "\n")
b.WriteString("Allowed Paths: " + strings.Join(server.AllowedPaths, ", ") + "\n")
return b.String()
}
+1 -1
View File
@@ -513,7 +513,7 @@ func (s *FileSystemServer) replaceFirstInFile(args map[string]any) (string, erro
find, ok := args["find"].(string)
if !ok || find == "" {
return "", fmt.Errorf("find text is required")
return "", fmt.Errorf("argument `find` is required")
}
replace, ok := args["replace"].(string)
-11
View File
@@ -20,17 +20,6 @@ type schedulerExt struct {
ListTasks() []any
GetTask(id string) any
}
mu interface {
Lock()
Unlock()
}
provider interface {
GetProvider(cfg any) (any, error)
}
providerMu interface {
Lock()
Unlock()
}
}
type TaskInfo struct {
+1 -8
View File
@@ -9,7 +9,6 @@ import (
"os/exec"
"runtime"
"strings"
"sync"
)
var DebugMode bool
@@ -19,12 +18,7 @@ type VisionResult struct {
Duration float64 `json:"duration_ms"`
}
var (
visionServer *exec.Cmd
visionServerOnce sync.Once
visionServerErr error
tesseractPath string
)
var visionServer *exec.Cmd
func visionDebug(format string, args ...any) {
if DebugMode {
@@ -185,7 +179,6 @@ func StopVisionServer() {
if visionServer != nil && visionServer.Process != nil {
visionServer.Process.Signal(os.Kill)
visionServer = nil
visionServerOnce = sync.Once{}
}
}
-5
View File
@@ -19,11 +19,6 @@ type hfRepoInfo struct {
} `json:"siblings"`
}
type hfFileInfo struct {
BlobHTTPURL string `json:"blobHTTPURL"`
Size int64 `json:"size"`
}
func getGGUFFiles(ctx context.Context, modelID string) ([]string, error) {
url := fmt.Sprintf("%s/models/%s", huggingfaceAPI, modelID)
+15 -16
View File
@@ -442,23 +442,22 @@ func promptQuantizationSelection(maxOptions int, defaultIdx int) (int, error) {
return 0, fmt.Errorf("no quantization options available")
}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("\nSelect quantization (default: %d for Q4_K_M): ", defaultIdx+1)
input, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
input = strings.TrimSpace(input)
if input == "" {
return defaultIdx, nil
}
idx, err := strconv.Atoi(input)
if err != nil || idx < 1 || idx > maxOptions {
fmt.Println("Invalid selection, using default Q4_K_M")
return defaultIdx, nil
}
return idx - 1, nil
fmt.Printf("\nSelect quantization (default: %d for Q4_K_M): ", defaultIdx+1)
input, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
input = strings.TrimSpace(input)
if input == "" {
return defaultIdx, nil
}
idx, err := strconv.Atoi(input)
if err != nil || idx < 1 || idx > maxOptions {
fmt.Println("Invalid selection, using default Q4_K_M")
return defaultIdx, nil
}
return idx - 1, nil
}
func getCachedQuantization(modelID string) (string, bool) {
+1 -10
View File
@@ -44,15 +44,6 @@ type openRouterModelsResponse struct {
Data []openRouterModelInfo `json:"data"`
}
type toolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
type responseToolCall struct {
ID string
Name string
@@ -681,7 +672,7 @@ func parseResponsesOutput(body []byte) (string, string, []responseToolCall, resp
case "function_call":
name, _ := item["name"].(string)
id, _ := item["id"].(string)
argsRaw, _ := item["arguments"]
argsRaw := item["arguments"]
argsStr := ""
switch v := argsRaw.(type) {
case string:
+1 -7
View File
@@ -365,7 +365,7 @@ func (s *Scheduler) saveTask(task *ScheduledTask) error {
lastRunStr = task.LastRun.Format(time.RFC3339)
}
_, err := s.db.Exec(`
INSERT OR REPLACE INTO scheduled_tasks
INSERT OR REPLACE INTO scheduled_tasks
(id, prompt, interval_sec, run_at, is_repeating, working_dir, created_at, last_run, run_count, last_error, last_output, provider_type, provider_name, provider_model)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, task.ID, task.Prompt, task.IntervalSec, task.RunAt, task.IsRepeating, task.WorkingDir,
@@ -867,9 +867,3 @@ func (s *Scheduler) Close() {
s.wg.Wait()
s.db.Close()
}
type providerConfig struct {
Type string `json:"type"`
Name string `json:"name"`
Model string `json:"model"`
}
+2 -5
View File
@@ -59,7 +59,7 @@ func RunWizard(destination string) error {
return defaultVal
}
if existingProvider.Type != "" && provider.Type != "" &&
strings.ToLower(existingProvider.Type) != strings.ToLower(provider.Type) {
!strings.EqualFold(existingProvider.Type, provider.Type) {
switch field {
case "model", "description", "endpoint", "api_key_env":
return defaultVal
@@ -337,10 +337,7 @@ func looksLikePromptEcho(input string) bool {
return true
}
}
if strings.Contains(input, "]:") {
return true
}
return false
return strings.Contains(input, "]:")
}
type selectOption struct {