Commit 2caa9cd

mo khan <mo@mokhan.ca>
2025-06-22 01:56:03
feat: shell out to the ollama cli
1 parent e04b47f
cmd/del/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"context"
 	"log"
 
 	"github.com/spf13/cobra"
@@ -8,31 +9,18 @@ import (
 )
 
 func main() {
-	var provider, model string
-
+	var model string
 	rootCmd := &cobra.Command{
 		Use:   "del",
-		Short: "Del the Funky Robosapien - Your AI-powered coding superhero",
+		Short: "Del the Funky Robosapien - Claude Code replacement with local AI",
 		Run: func(cmd *cobra.Command, args []string) {
-			var ai del.AIProvider
-
-			switch provider {
-			case "claude":
-				ai = del.NewClaudeProvider("")
-			case "ollama":
-				ai = del.NewOllamaProvider(model)
-			default:
-				ai = del.AutoProvider("", model)
-			}
-
+			ai := del.NewOllamaProvider(model)
 			assistant := del.NewDel(ai)
-			assistant.StartREPL()
+			assistant.StartREPL(context.Background())
 		},
 	}
 
-	rootCmd.Flags().StringVarP(&provider, "provider", "p", "auto", "AI provider (claude, ollama, auto)")
-	rootCmd.Flags().StringVarP(&model, "model", "m", "deepseek-coder-v2:16b", "Model to use (for Ollama)")
-
+	rootCmd.Flags().StringVarP(&model, "model", "m", "deepseek-coder-v2:16b", "Ollama model to use")
 	if err := rootCmd.Execute(); err != nil {
 		log.Fatal(err)
 	}
internal/del/assistant.go
@@ -2,34 +2,29 @@ package del
 
 import (
 	"bufio"
+	"context"
+	"encoding/json"
 	"fmt"
 	"os"
 	"strings"
 )
 
-type Tool struct {
-	Name        string
-	Description string
-	Handler     func(args map[string]interface{}) (interface{}, error)
-}
-
 type Del struct {
 	aiProvider AIProvider
 	tools      map[string]*Tool
-	mcpServers map[string]string
 }
 
-func NewDel(provider AIProvider) *Del {
-	return &Del{
-		aiProvider: provider,
-		tools:      make(map[string]*Tool),
-		mcpServers: make(map[string]string),
+func NewDel(ai AIProvider) *Del {
+	d := &Del{
+		aiProvider: ai,
+		tools:      map[string]*Tool{},
 	}
+	d.registerTools()
+	return d
 }
 
-func (d *Del) StartREPL() {
-	fmt.Printf("๐ŸŽค Del (%s) is ready!\n", d.aiProvider.Name())
-	fmt.Println("Type 'quit' to exit.")
+func (d *Del) StartREPL(ctx context.Context) {
+	fmt.Println("๐ŸŽค Del is ready with", d.aiProvider.Name())
 	scanner := bufio.NewScanner(os.Stdin)
 
 	for {
@@ -37,11 +32,63 @@ func (d *Del) StartREPL() {
 		if !scanner.Scan() {
 			break
 		}
-		input := strings.TrimSpace(scanner.Text())
-		if input == "quit" || input == "exit" {
-			fmt.Println("๐ŸŽค Del says: Peace out! โœŒ๏ธ")
-			break
+		line := scanner.Text()
+		if strings.HasPrefix(line, "/") {
+			d.handleSlash(line)
+			continue
+		}
+
+		prompt := d.buildPrompt(line)
+		response, err := d.aiProvider.Generate(ctx, prompt)
+		if err != nil {
+			fmt.Println("โŒ Error:", err)
+			continue
+		}
+
+		fmt.Println("๐Ÿค– Del:", response)
+		d.handleToolResponse(response)
+	}
+}
+
+func (d *Del) handleSlash(line string) {
+	fields := strings.Fields(line)
+	switch fields[0] {
+	case "/quit":
+		fmt.Println("๐Ÿ‘‹ Bye!")
+		os.Exit(0)
+	case "/model":
+		fmt.Println("Current model:", d.aiProvider.Name())
+	default:
+		fmt.Println("โ“ Unknown command:", line)
+	}
+}
+
+func (d *Del) handleToolResponse(response string) {
+	for _, line := range strings.Split(response, " ") {
+		if strings.HasPrefix(line, "TOOL_USE:") {
+			parts := strings.SplitN(line[len("TOOL_USE:"):], " ", 2)
+			if len(parts) != 2 {
+				fmt.Println("โŒ Malformed tool call:", line)
+				continue
+			}
+			name := strings.TrimSpace(parts[0])
+			argsJSON := strings.TrimSpace(parts[1])
+			tool, ok := d.tools[name]
+			if !ok {
+				fmt.Println("โŒ Unknown tool:", name)
+				continue
+			}
+			var args map[string]interface{}
+			if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
+				fmt.Println("โŒ JSON error:", err)
+				continue
+			}
+			result, err := tool.Handler(args)
+			if err != nil {
+				fmt.Println("โŒ Tool error:", err)
+			} else {
+				fmt.Printf("โœ… %s result: %+v ", name, result)
+			}
 		}
-		fmt.Printf("๐Ÿค– (mock reply): You said '%s'\n", input)
 	}
 }
internal/del/prompt.go
@@ -0,0 +1,44 @@
+package del
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+func (d *Del) buildPrompt(input string) string {
+	tools := []string{}
+	for name, tool := range d.tools {
+		tools = append(tools, fmt.Sprintf("- %s: %s", name, tool.Description))
+	}
+
+	return fmt.Sprintf(`You are Del the Funky Robosapien ๐ŸŽค๐Ÿค–
+
+Available tools:
+%s
+
+Working dir: %s
+Git status:
+%s
+
+User input:
+%s
+
+If you need a tool, respond like:
+TOOL_USE: read_file {"path": "main.go"}
+`, strings.Join(tools, " "), getCWD(), getGitStatus(), input)
+}
+
+func getCWD() string {
+	cwd, _ := os.Getwd()
+	return cwd
+}
+
+func getGitStatus() string {
+	out, err := exec.Command("git", "status", "--short").Output()
+	if err != nil {
+		return "Not a git repo"
+	}
+	return string(out)
+}
internal/del/provider.go
@@ -1,61 +1,33 @@
 package del
 
 import (
-	"context"
-	"fmt"
-	"os/exec"
+    "context"
+    "fmt"
+    "os/exec"
 )
 
 type AIProvider interface {
-	Generate(ctx context.Context, prompt string) (string, error)
-	Name() string
-}
-
-type ClaudeProvider struct {
-	apiKey string
-}
-
-func NewClaudeProvider(apiKey string) AIProvider {
-	return &ClaudeProvider{apiKey: apiKey}
-}
-
-func (c *ClaudeProvider) Generate(ctx context.Context, prompt string) (string, error) {
-	cmd := exec.CommandContext(ctx, "claude", "--print", prompt)
-	output, err := cmd.Output()
-	if err != nil {
-		return "", fmt.Errorf("claude error: %w", err)
-	}
-	return string(output), nil
-}
-
-func (c *ClaudeProvider) Name() string {
-	return "Claude"
+    Generate(ctx context.Context, prompt string) (string, error)
+    Name() string
 }
 
 type OllamaProvider struct {
-	model string
+    model string
 }
 
 func NewOllamaProvider(model string) AIProvider {
-	return &OllamaProvider{model: model}
+    return &OllamaProvider{model: model}
 }
 
 func (o *OllamaProvider) Generate(ctx context.Context, prompt string) (string, error) {
-	cmd := exec.CommandContext(ctx, "ollama", "run", o.model, prompt)
-	output, err := cmd.Output()
-	if err != nil {
-		return "", fmt.Errorf("ollama error: %w", err)
-	}
-	return string(output), nil
+    cmd := exec.CommandContext(ctx, "ollama", "run", o.model, prompt)
+    output, err := cmd.CombinedOutput()
+    if err != nil {
+        return "", fmt.Errorf("ollama error: %w\n%s", err, string(output))
+    }
+    return string(output), nil
 }
 
 func (o *OllamaProvider) Name() string {
-	return fmt.Sprintf("Ollama (%s)", o.model)
-}
-
-func AutoProvider(apiKey, model string) AIProvider {
-	if exec.Command("claude", "--version").Run() == nil {
-		return NewClaudeProvider(apiKey)
-	}
-	return NewOllamaProvider(model)
+    return fmt.Sprintf("Ollama (%s)", o.model)
 }
internal/del/tools.go
@@ -0,0 +1,66 @@
+package del
+
+import (
+    "fmt"
+    "os"
+    "os/exec"
+)
+
+type Tool struct {
+    Name        string
+    Description string
+    Handler     func(args map[string]interface{}) (interface{}, error)
+}
+
+func (d *Del) registerTools() {
+    d.tools["read_file"] = &Tool{"read_file", "Read a file from disk", d.readFile}
+    d.tools["write_file"] = &Tool{"write_file", "Write content to a file", d.writeFile}
+    d.tools["run_command"] = &Tool{"run_command", "Execute a shell command", d.runCommand}
+    d.tools["analyze_code"] = &Tool{"analyze_code", "Analyze code quality", d.analyzeCode}
+    d.tools["mcp_git_list"] = &Tool{"mcp_git_list", "MCP: List repo files", d.stubMCP}
+    d.tools["mcp_git_read"] = &Tool{"mcp_git_read", "MCP: Read repo file", d.stubMCP}
+}
+
+func (d *Del) readFile(args map[string]interface{}) (interface{}, error) {
+    path, ok := args["path"].(string)
+    if !ok {
+        return nil, fmt.Errorf("missing 'path'")
+    }
+    data, err := os.ReadFile(path)
+    if err != nil {
+        return nil, err
+    }
+    return string(data), nil
+}
+
+func (d *Del) writeFile(args map[string]interface{}) (interface{}, error) {
+    path, ok := args["path"].(string)
+    content, ok2 := args["content"].(string)
+    if !ok || !ok2 {
+        return nil, fmt.Errorf("missing 'path' or 'content'")
+    }
+    return "written", os.WriteFile(path, []byte(content), 0644)
+}
+
+func (d *Del) runCommand(args map[string]interface{}) (interface{}, error) {
+    command, ok := args["command"].(string)
+    if !ok {
+        return nil, fmt.Errorf("missing 'command'")
+    }
+    output, err := exec.Command("sh", "-c", command).CombinedOutput()
+    return string(output), err
+}
+
+func (d *Del) analyzeCode(args map[string]interface{}) (interface{}, error) {
+    return map[string]interface{}{
+        "quality": "good",
+        "suggestions": []string{
+            "Consider breaking large functions into smaller ones.",
+            "Add more comments for readability.",
+        },
+    }, nil
+}
+
+func (d *Del) stubMCP(args map[string]interface{}) (interface{}, error) {
+    return map[string]string{"status": "MCP stub - not connected"}, nil
+}