Commit 4e32ac4

mo khan <mo@mokhan.ca>
2025-06-22 05:02:36
feat: add more tools
1 parent 75e6850
Changed files (1)
cmd
cmd/del/main.go
@@ -110,6 +110,211 @@ func (o *OllamaProvider) Name() string {
 	return fmt.Sprintf("Ollama (%s)", o.model)
 }
 
+func (d *Del) runCommand(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	cmdStr, ok := args["command"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'command' string argument")
+	}
+	cmd := exec.CommandContext(ctx, "bash", "-c", cmdStr)
+	cmd.Env = os.Environ()
+	f, err := pty.Start(cmd)
+	if err != nil {
+		return nil, fmt.Errorf("failed to start PTY: %w", err)
+	}
+	defer func() { _ = f.Close() }()
+	scanner := bufio.NewScanner(f)
+	var output strings.Builder
+	var anyOutput bool
+	for scanner.Scan() {
+		line := scanner.Text()
+		output.WriteString(line + "\n")
+		ch <- line + "\n"
+		anyOutput = true
+	}
+	if scanErr := scanner.Err(); scanErr != nil && !anyOutput {
+		return output.String(), fmt.Errorf("scanner error: %w", scanErr)
+	}
+	if err := cmd.Wait(); err != nil {
+		return output.String(), fmt.Errorf("command failed: %w", err)
+	}
+	return output.String(), nil
+}
+
+// read_file
+func (d *Del) readFile(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	path, ok := args["path"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'path' string argument")
+	}
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	ch <- string(data)
+	return string(data), nil
+}
+
+// write_file
+func (d *Del) writeFile(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	path, ok1 := args["path"].(string)
+	content, ok2 := args["content"].(string)
+	if !ok1 || !ok2 {
+		return nil, fmt.Errorf("missing 'path' or 'content' argument")
+	}
+	if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+		return nil, err
+	}
+	msg := fmt.Sprintf("wrote %d bytes to %s", len(content), path)
+	ch <- msg
+	return msg, nil
+}
+
+// list_dir
+func (d *Del) listDir(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	dir, _ := args["path"].(string)
+	if dir == "" {
+		dir = "."
+	}
+	entries, err := os.ReadDir(dir)
+	if err != nil {
+		return nil, err
+	}
+	var names []string
+	for _, entry := range entries {
+		names = append(names, entry.Name())
+	}
+	output := strings.Join(names, "\n")
+	ch <- output
+	return output, nil
+}
+
+// analyze_code (counts lines, functions, basic complexity for Go/Python/JS)
+func (d *Del) analyzeCode(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	content, ok := args["content"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'content' string argument")
+	}
+	lines := strings.Count(content, "\n") + 1
+	funcs := regexp.MustCompile(`(?m)^[ \t]*(func |def |function )`).FindAllStringIndex(content, -1)
+	out := fmt.Sprintf("Lines: %d\nFunctions: %d", lines, len(funcs))
+	ch <- out
+	return out, nil
+}
+
+// extract_functions (Go, Python, JS regexes)
+func (d *Del) extractFunctions(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	content, ok := args["content"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'content' string argument")
+	}
+	var re = regexp.MustCompile(`(?m)^[ \t]*(func|def|function)\s+([\w_]+)`) // captures func name
+	matches := re.FindAllStringSubmatch(content, -1)
+	var names []string
+	for _, m := range matches {
+		names = append(names, m[2])
+	}
+	output := strings.Join(names, "\n")
+	ch <- output
+	return output, nil
+}
+
+// find_references (grep)
+func (d *Del) findReferences(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	symbol, ok := args["symbol"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'symbol' string argument")
+	}
+	path, _ := args["path"].(string)
+	if path == "" {
+		path = "."
+	}
+	cmd := exec.Command("grep", "-rnw", path, "-e", symbol)
+	out, err := cmd.CombinedOutput()
+	result := string(out)
+	ch <- result
+	return result, err
+}
+
+// search_code (pattern search)
+func (d *Del) searchCode(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	pattern, ok := args["pattern"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'pattern' string argument")
+	}
+	path, _ := args["path"].(string)
+	if path == "" {
+		path = "."
+	}
+	cmd := exec.Command("grep", "-rnIH", pattern, path)
+	out, err := cmd.CombinedOutput()
+	result := string(out)
+	ch <- result
+	return result, err
+}
+
+// format_code (auto-formats via gofmt/black/prettier/etc based on extension)
+func (d *Del) formatCode(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	path, ok := args["path"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'path' string argument")
+	}
+	ext := ""
+	if dot := strings.LastIndex(path, "."); dot != -1 {
+		ext = path[dot+1:]
+	}
+	var cmd *exec.Cmd
+	switch ext {
+	case "go":
+		cmd = exec.Command("gofmt", "-w", path)
+	case "py":
+		cmd = exec.Command("black", path)
+	case "js", "ts":
+		cmd = exec.Command("prettier", "--write", path)
+	default:
+		return nil, fmt.Errorf("unsupported format for extension: %s", ext)
+	}
+	out, err := cmd.CombinedOutput()
+	result := string(out)
+	ch <- result
+	return result, err
+}
+
+// lint_code
+func (d *Del) lintCode(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	path, ok := args["path"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'path' string argument")
+	}
+	ext := ""
+	if dot := strings.LastIndex(path, "."); dot != -1 {
+		ext = path[dot+1:]
+	}
+	var cmd *exec.Cmd
+	switch ext {
+	case "go":
+		cmd = exec.Command("golint", path)
+	case "py":
+		cmd = exec.Command("pylint", path)
+	case "js", "ts":
+		cmd = exec.Command("eslint", path)
+	default:
+		return nil, fmt.Errorf("unsupported linter for extension: %s", ext)
+	}
+	out, err := cmd.CombinedOutput()
+	result := string(out)
+	ch <- result
+	return result, err
+}
+
+// mcp_call (stub for now)
+func (d *Del) stubMCP(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	endpoint, _ := args["endpoint"].(string)
+	payload, _ := args["payload"].(string)
+	msg := fmt.Sprintf("[stub] Would POST to MCP '%s' with payload: %s", endpoint, payload)
+	ch <- msg
+	return msg, nil
+}
+
 func NewDel(provider AIProvider) *Del {
 	d := &Del{
 		aiProvider: provider,
@@ -124,88 +329,58 @@ func NewDel(provider AIProvider) *Del {
 	d.tools["run_command"] = &Tool{
 		Name:        "run_command",
 		Description: "Execute a shell command and return the output",
-		Handler: func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
-			cmdStr, ok := args["command"].(string)
-			if !ok {
-				return nil, fmt.Errorf("missing 'command' string argument")
-			}
-			cmd := exec.CommandContext(ctx, "bash", "-c", cmdStr)
-			cmd.Env = os.Environ()
-			f, err := pty.Start(cmd)
-			if err != nil {
-				return nil, fmt.Errorf("failed to start PTY: %w", err)
-			}
-			defer func() { _ = f.Close() }()
-			scanner := bufio.NewScanner(f)
-			var output strings.Builder
-			var anyOutput bool
-			for scanner.Scan() {
-				line := scanner.Text()
-				output.WriteString(line + "\n")
-				ch <- line + "\n"
-				anyOutput = true
-			}
-			if scanErr := scanner.Err(); scanErr != nil && !anyOutput {
-				return output.String(), fmt.Errorf("scanner error: %w", scanErr)
-			}
-			if err := cmd.Wait(); err != nil {
-				return output.String(), fmt.Errorf("command failed: %w", err)
-			}
-			return output.String(), nil
-		},
+		Handler:     d.runCommand,
 	}
 	d.tools["read_file"] = &Tool{
 		Name:        "read_file",
 		Description: "Reads the contents of a file",
-		Handler: func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
-			path, ok := args["path"].(string)
-			if !ok {
-				return nil, fmt.Errorf("missing 'path' string argument")
-			}
-			data, err := os.ReadFile(path)
-			if err != nil {
-				return nil, err
-			}
-			return string(data), nil
-		},
+		Handler:     d.readFile,
 	}
 	d.tools["write_file"] = &Tool{
 		Name:        "write_file",
 		Description: "Writes content to a file",
-		Handler: func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
-			path, ok1 := args["path"].(string)
-			content, ok2 := args["content"].(string)
-			if !ok1 || !ok2 {
-				return nil, fmt.Errorf("missing 'path' or 'content' argument")
-			}
-			if err := os.WriteFile(path, []byte(content), 0644); err != nil {
-				return nil, err
-			}
-			return fmt.Sprintf("wrote %d bytes to %s", len(content), path), nil
-		},
+		Handler:     d.writeFile,
+	}
+	d.tools["list_dir"] = &Tool{
+		Name:        "list_dir",
+		Description: "Lists files in a directory",
+		Handler:     d.listDir,
 	}
 	d.tools["analyze_code"] = &Tool{
 		Name:        "analyze_code",
 		Description: "Analyze code",
-		Handler: func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
-			content, ok := args["content"].(string)
-			if !ok {
-				return nil, fmt.Errorf("missing 'content' string argument")
-			}
-			lines := strings.Count(content, "\n")
-			return fmt.Sprintf("Code has %d lines. Potential for refactoring if > 200 lines.", lines), nil
-		},
+		Handler:     d.analyzeCode,
+	}
+	d.tools["extract_functions"] = &Tool{
+		Name:        "extract_functions",
+		Description: "Extracts all function names and signatures from a source file",
+		Handler:     d.extractFunctions,
+	}
+	d.tools["find_references"] = &Tool{
+		Name:        "find_references",
+		Description: "Finds references to a symbol (variable, function, etc) in a directory or project",
+		Handler:     d.findReferences,
+	}
+	d.tools["search_code"] = &Tool{
+		Name:        "search_code",
+		Description: "Searches for a pattern (string or regex) in the codebase",
+		Handler:     d.searchCode,
+	}
+	d.tools["format_code"] = &Tool{
+		Name:        "format_code",
+		Description: "Auto-formats source code using language-specific formatters (gofmt, prettier, black, etc)",
+		Handler:     d.formatCode,
+	}
+	d.tools["lint_code"] = &Tool{
+		Name:        "lint_code",
+		Description: "Runs a linter on the source code and returns warnings/errors",
+		Handler:     d.lintCode,
 	}
 	d.tools["mcp_call"] = &Tool{
 		Name:        "mcp_call",
 		Description: "Stub for MCP integration",
-		Handler: func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
-			endpoint, _ := args["endpoint"].(string)
-			payload, _ := args["payload"].(string)
-			return fmt.Sprintf("[stub] Would send POST to MCP '%s' with payload: %s", endpoint, payload), nil
-		},
+		Handler:     d.stubMCP,
 	}
-
 	return d
 }