Commit 1ca9e83
cmd/del/main.go
@@ -10,1030 +10,208 @@ import (
"os/exec"
"regexp"
"strings"
+ "time"
- "github.com/creack/pty"
"github.com/ollama/ollama/api"
)
-const SystemPrompt = `
-You are Del the Funky Robosapien π€π€ - an elite coding assistant with deep expertise in software development.
-
-# YOUR MISSION
-Help developers write better, safer, and more efficient code through intelligent tool usage and expert guidance.
-
-# CORE BEHAVIOR
-
-1. **Smart Tool Usage**: When users need file operations, code analysis, or system commands, use your available function calling capabilities to invoke the appropriate tools automatically. **IMPORTANT: STOP immediately after making tool calls - do NOT generate fake results or continue the response.**
-
-2. **Expert Coding Advice**: Provide insightful, actionable coding guidance when tools aren't needed
-
-3. **Context-Aware**: Understand the project structure and programming languages in use
-
-4. **Security-First**: Always consider security implications and suggest best practices
-
-# AVAILABLE TOOLS
-
-π§ Core Tools:
-- run_command: Execute shell commands {"command": string}
-- read_file: Read and analyze files {"path": string}
-- write_file: Write code to files {"path": string, "content": string}
-- list_dir: List directory contents {"path": string}
-- project_context: Analyze current project structure {}
-
-π Code Analysis:
-- analyze_code: Deep code analysis {"content": string}
-- extract_functions: Find all functions {"content": string}
-- find_references: Search for symbol usage {"symbol": string, "path": string}
-- search_code: Pattern search in codebase {"pattern": string, "path": string}
-
-π‘οΈ Quality & Security:
-- security_scan: Vulnerability scanning {"path": string}
-- format_code: Auto-format code {"path": string}
-- lint_code: Run linters {"path": string}
-
-π Git Operations:
-- git_operation: Git commands {"operation": string, "files": string, "message": string, "branch": string}
-
-π Web & Testing:
-- web_search: Search for information {"query": string}
-- test_runner: Run project tests {"type": string, "path": string}
-
-π Multi-File Operations:
-- multi_edit: Edit multiple files with find-replace {"operations": [{"file": string, "old": string, "new": string}]}
-- batch_operations: Execute multiple file operations {"operations": [{"type": "read|write|list", "path": string, "content": string}]}
-
-# INTERACTION STYLE
-
-β
DO:
-- Use tools when users need file ops, analysis, or commands
-- STOP immediately after making tool calls (let Del execute them)
-- Give concise, expert coding advice when tools aren't needed
-- Suggest best practices and optimizations
-- Be proactive about security and code quality
-
-β DON'T:
-- Generate fake tool outputs or results
-- Continue responding after making tool calls
-- Add <ο½toolβoutputsβbeginο½> or similar fake output blocks
-- Over-explain tool usage
-- Apologize unnecessarily
-
-# EXAMPLES
-
-When users make requests that require tools, automatically invoke the appropriate functions:
-
-User: "show me the main function" β read_file with path "main.go"
-User: "what's the git status?" β git_operation with operation "status"
-User: "scan this project for security issues" β security_scan with path "."
-User: "run the tests" β test_runner to auto-detect and run tests
-User: "search for golang best practices" β web_search with relevant query
-User: "how do I optimize this algorithm?" β provide expert advice (no tools needed)
-
-Use your natural function calling format - Del supports all major tool calling standards.
-
-Stay funky, keep coding! π΅`
-
-type Tool struct {
- Name string
- Description string
- Handler func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error)
+// Del implements Claude Code style interface using Ollama
+type Del struct {
+ client *api.Client
+ model string
+ chatHistory []api.Message
+ tools map[string]ToolFunc
}
+type ToolFunc func(ctx context.Context, args map[string]interface{}) (string, error)
+
type ToolCall struct {
Name string
Args map[string]interface{}
}
-type Del struct {
- aiProvider AIProvider
- tools map[string]*Tool
- chatHistory []api.Message
- safeMode bool
- confirmedActions map[string]bool
-}
-
-type AIProvider interface {
- Chat(ctx context.Context, history []api.Message) (string, error)
- StreamChat(ctx context.Context, history []api.Message) (string, error)
- Name() string
-}
-
-type OllamaProvider struct {
- model string
- client *api.Client
-}
-
-
-func NewOllamaProvider(model string) *OllamaProvider {
+func NewDel(model string) *Del {
client, _ := api.ClientFromEnvironment()
- return &OllamaProvider{model: model, client: client}
-}
-
-func (o *OllamaProvider) Chat(ctx context.Context, history []api.Message) (string, error) {
- var full string
- err := o.client.Chat(ctx, &api.ChatRequest{
- Model: o.model,
- Messages: history,
- }, func(resp api.ChatResponse) error {
- full += resp.Message.Content
- return nil
- })
- if err != nil {
- return "", fmt.Errorf("ollama API error: %w", err)
- }
- return full, nil
-}
-
-func (o *OllamaProvider) StreamChat(ctx context.Context, history []api.Message) (string, error) {
- var full string
- err := o.client.Chat(ctx, &api.ChatRequest{
- Model: o.model,
- Messages: history,
- }, func(resp api.ChatResponse) error {
- content := resp.Message.Content
- full += content
- fmt.Print(content) // Stream output to terminal
- return nil
- })
- if err != nil {
- return "", fmt.Errorf("ollama API error: %w", err)
+
+ d := &Del{
+ client: client,
+ model: model,
+ tools: make(map[string]ToolFunc),
+ chatHistory: []api.Message{
+ {
+ Role: "system",
+ Content: `You are Del, an AI coding assistant. When users need file operations or code analysis, use your available tools.
+
+Available tools:
+- read_file: Read file contents
+- list_dir: List directory contents
+- run_command: Execute shell commands
+- git_status: Check git repository status
+- write_file: Write content to files
+- analyze_code: Analyze code structure
+- search_code: Search for patterns in code
+
+Use tools by generating calls in this format:
+function<ο½toolβsepο½>tool_name
+{"arg": "value"}
+
+IMPORTANT: Stop immediately after making tool calls. Don't generate fake results.`,
+ },
+ },
}
- return full, nil
-}
-
-func (o *OllamaProvider) Name() string {
- return fmt.Sprintf("Ollama (%s)", o.model)
+
+ // Register tools
+ d.registerTools()
+ return d
}
-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
+func (d *Del) registerTools() {
+ d.tools["read_file"] = d.readFile
+ d.tools["list_dir"] = d.listDir
+ d.tools["run_command"] = d.runCommand
+ d.tools["git_status"] = d.gitStatus
+ d.tools["write_file"] = d.writeFile
+ d.tools["analyze_code"] = d.analyzeCode
+ d.tools["search_code"] = d.searchCode
}
-// read_file
-func (d *Del) readFile(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+func (d *Del) readFile(ctx context.Context, args map[string]interface{}) (string, error) {
path, ok := args["path"].(string)
if !ok {
- return nil, fmt.Errorf("missing 'path' string argument")
+ return "", fmt.Errorf("missing 'path' argument")
}
+
data, err := os.ReadFile(path)
if err != nil {
- return nil, err
+ return "", 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)
+
+ lines := strings.Split(string(data), "\n")
+ if len(lines) > 50 {
+ return fmt.Sprintf("Read %s (%d lines, showing first 50)\n%s\n... (truncated)",
+ path, len(lines), strings.Join(lines[:50], "\n")), nil
}
- out, err := cmd.CombinedOutput()
- result := string(out)
- ch <- result
- return result, err
+
+ return fmt.Sprintf("Read %s (%d lines)\n%s", path, len(lines), string(data)), nil
}
-// lint_code
-func (d *Del) lintCode(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+func (d *Del) listDir(ctx context.Context, args map[string]interface{}) (string, 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
-}
-
-// security_scan
-func (d *Del) securityScan(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
- path, _ := args["path"].(string)
- if path == "" {
path = "."
}
- var output strings.Builder
-
- // Try gosec for Go files
- if cmd := exec.Command("gosec", "-quiet", path); cmd != nil {
- if out, err := cmd.CombinedOutput(); err == nil {
- output.WriteString("=== gosec results ===\n")
- output.WriteString(string(out))
- output.WriteString("\n")
- }
- }
-
- // Try semgrep if available
- if cmd := exec.Command("semgrep", "--config=auto", path); cmd != nil {
- if out, err := cmd.CombinedOutput(); err == nil {
- output.WriteString("=== semgrep results ===\n")
- output.WriteString(string(out))
- output.WriteString("\n")
- }
- }
-
- if output.Len() == 0 {
- output.WriteString("No security scanning tools found (gosec, semgrep)")
- }
-
- result := output.String()
- ch <- result
- return result, nil
-}
-
-// project_context: Analyzes current project structure and provides context
-func (d *Del) projectContext(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
- var output strings.Builder
-
- // Get current working directory
- cwd, _ := os.Getwd()
- output.WriteString(fmt.Sprintf("π Project: %s\n\n", cwd))
-
- // Detect project type based on config files
- projectTypes := map[string]string{
- "go.mod": "πΉ Go project",
- "package.json": "π¦ Node.js/JavaScript project",
- "Cargo.toml": "π¦ Rust project",
- "requirements.txt": "π Python project",
- "pom.xml": "β Java (Maven) project",
- "Gemfile": "π Ruby project",
- "composer.json": "π PHP project",
- "Makefile": "π¨ C/C++ project",
- }
-
- output.WriteString("π Project Type:\n")
- found := false
- for file, desc := range projectTypes {
- if _, err := os.Stat(file); err == nil {
- output.WriteString(fmt.Sprintf(" %s\n", desc))
- found = true
- }
- }
- if !found {
- output.WriteString(" π Generic project\n")
- }
-
- // List key files
- output.WriteString("\nπ Key Files:\n")
- entries, err := os.ReadDir(".")
- if err == nil {
- importantFiles := []string{}
- for _, entry := range entries {
- name := entry.Name()
- if !entry.IsDir() && isImportantFile(name) {
- importantFiles = append(importantFiles, name)
- }
- }
- for _, file := range importantFiles {
- output.WriteString(fmt.Sprintf(" β’ %s\n", file))
- }
- }
-
- // Show directory structure (top level)
- output.WriteString("\nπ Directory Structure:\n")
- if entries != nil {
- dirs := []string{}
- for _, entry := range entries {
- if entry.IsDir() && !strings.HasPrefix(entry.Name(), ".") {
- dirs = append(dirs, entry.Name())
- }
- }
- for _, dir := range dirs {
- output.WriteString(fmt.Sprintf(" π %s/\n", dir))
- }
- }
-
- result := output.String()
- ch <- result
- return result, nil
-}
-
-func isImportantFile(name string) bool {
- important := []string{
- "main.go", "main.py", "main.js", "index.js", "app.js", "server.js",
- "README.md", "README.txt", "LICENSE", "Dockerfile", ".gitignore",
- "go.mod", "package.json", "requirements.txt", "Cargo.toml", "Makefile",
- "pom.xml", "build.gradle", "composer.json", "Gemfile",
+ entries, err := os.ReadDir(path)
+ if err != nil {
+ return "", err
}
- for _, imp := range important {
- if name == imp {
- return true
- }
- }
+ var result strings.Builder
+ result.WriteString(fmt.Sprintf("List %s:\n", path))
- // Also include files with common extensions
- extensions := []string{".md", ".go", ".py", ".js", ".ts", ".rs", ".java", ".c", ".cpp", ".h"}
- for _, ext := range extensions {
- if strings.HasSuffix(name, ext) && len(name) < 20 { // Avoid very long filenames
- return true
+ for _, entry := range entries {
+ if entry.IsDir() {
+ result.WriteString(fmt.Sprintf(" π %s/\n", entry.Name()))
+ } else {
+ result.WriteString(fmt.Sprintf(" π %s\n", entry.Name()))
}
}
- return false
+ return result.String(), nil
}
-// git_operation: Execute git commands safely
-func (d *Del) gitOperation(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
- operation, ok := args["operation"].(string)
+func (d *Del) runCommand(ctx context.Context, args map[string]interface{}) (string, error) {
+ command, ok := args["command"].(string)
if !ok {
- return nil, fmt.Errorf("missing 'operation' string argument")
- }
-
- // Parse additional args
- files, _ := args["files"].(string)
- message, _ := args["message"].(string)
- branch, _ := args["branch"].(string)
- remote, _ := args["remote"].(string)
-
- var cmd *exec.Cmd
-
- switch operation {
- case "status":
- cmd = exec.CommandContext(ctx, "git", "status", "--porcelain=v1")
-
- case "status-verbose", "status-v":
- cmd = exec.CommandContext(ctx, "git", "status")
-
- case "add":
- if files == "" {
- files = "."
- }
- cmd = exec.CommandContext(ctx, "git", "add", files)
-
- case "commit":
- if message == "" {
- return nil, fmt.Errorf("commit operation requires 'message' argument")
- }
- cmd = exec.CommandContext(ctx, "git", "commit", "-m", message)
-
- case "push":
- if remote == "" {
- remote = "origin"
- }
- if branch == "" {
- cmd = exec.CommandContext(ctx, "git", "push", remote)
- } else {
- cmd = exec.CommandContext(ctx, "git", "push", remote, branch)
- }
-
- case "pull":
- if remote == "" {
- remote = "origin"
- }
- cmd = exec.CommandContext(ctx, "git", "pull", remote)
-
- case "diff":
- if files == "" {
- cmd = exec.CommandContext(ctx, "git", "diff")
- } else {
- cmd = exec.CommandContext(ctx, "git", "diff", files)
- }
-
- case "diff-staged":
- cmd = exec.CommandContext(ctx, "git", "diff", "--staged")
-
- case "log":
- cmd = exec.CommandContext(ctx, "git", "log", "--oneline", "-10")
-
- case "log-verbose":
- cmd = exec.CommandContext(ctx, "git", "log", "-5", "--stat")
-
- case "branch":
- cmd = exec.CommandContext(ctx, "git", "branch", "-a")
-
- case "checkout":
- if branch == "" {
- return nil, fmt.Errorf("checkout operation requires 'branch' argument")
- }
- cmd = exec.CommandContext(ctx, "git", "checkout", branch)
-
- case "create-branch":
- if branch == "" {
- return nil, fmt.Errorf("create-branch operation requires 'branch' argument")
- }
- cmd = exec.CommandContext(ctx, "git", "checkout", "-b", branch)
-
- case "merge":
- if branch == "" {
- return nil, fmt.Errorf("merge operation requires 'branch' argument")
- }
- cmd = exec.CommandContext(ctx, "git", "merge", branch)
-
- case "stash":
- cmd = exec.CommandContext(ctx, "git", "stash")
-
- case "stash-pop":
- cmd = exec.CommandContext(ctx, "git", "stash", "pop")
-
- case "stash-list":
- cmd = exec.CommandContext(ctx, "git", "stash", "list")
-
- case "remote":
- cmd = exec.CommandContext(ctx, "git", "remote", "-v")
-
- case "reset":
- if files == "" {
- cmd = exec.CommandContext(ctx, "git", "reset", "--soft", "HEAD~1")
- } else {
- cmd = exec.CommandContext(ctx, "git", "reset", "HEAD", files)
- }
-
- case "clean":
- cmd = exec.CommandContext(ctx, "git", "clean", "-fd")
-
- default:
- return nil, fmt.Errorf("unsupported git operation: %s", operation)
+ return "", fmt.Errorf("missing 'command' argument")
}
+ cmd := exec.CommandContext(ctx, "sh", "-c", command)
output, err := cmd.CombinedOutput()
- result := string(output)
if err != nil {
- result = fmt.Sprintf("Git command failed: %s\nOutput: %s", err, result)
+ return fmt.Sprintf("Command: %s\nError: %v\nOutput: %s", command, err, string(output)), nil
}
- ch <- result
- return result, nil
+ return fmt.Sprintf("Command: %s\nOutput:\n%s", command, string(output)), nil
}
-// web_search: Search the web for information
-func (d *Del) webSearch(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
- query, ok := args["query"].(string)
- if !ok {
- return nil, fmt.Errorf("missing 'query' string argument")
- }
-
- // Use curl to search DuckDuckGo (simple text API)
- searchURL := fmt.Sprintf("https://api.duckduckgo.com/?q=%s&format=json&no_html=1&skip_disambig=1",
- strings.ReplaceAll(query, " ", "+"))
+func (d *Del) gitStatus(ctx context.Context, args map[string]interface{}) (string, error) {
+ cmd := exec.CommandContext(ctx, "git", "status", "--porcelain")
+ output, err := cmd.CombinedOutput()
- cmd := exec.CommandContext(ctx, "curl", "-s", searchURL)
- output, err := cmd.Output()
if err != nil {
- return nil, fmt.Errorf("web search failed: %w", err)
- }
-
- // Parse the JSON response for abstract/answer
- var response map[string]interface{}
- if err := json.Unmarshal(output, &response); err != nil {
- return string(output), nil // Return raw if parsing fails
+ return "Not a git repository or git not available", nil
}
- var result strings.Builder
- result.WriteString(fmt.Sprintf("π Search: %s\n\n", query))
-
- if abstract, ok := response["Abstract"].(string); ok && abstract != "" {
- result.WriteString(fmt.Sprintf("π Summary: %s\n\n", abstract))
+ if len(output) == 0 {
+ return "Git status: Clean working directory", nil
}
- if answer, ok := response["Answer"].(string); ok && answer != "" {
- result.WriteString(fmt.Sprintf("π‘ Answer: %s\n\n", answer))
- }
-
- if relatedTopics, ok := response["RelatedTopics"].([]interface{}); ok && len(relatedTopics) > 0 {
- result.WriteString("π Related:\n")
- for i, topic := range relatedTopics {
- if i >= 3 {
- break // Limit to 3 results
- }
- if topicMap, ok := topic.(map[string]interface{}); ok {
- if text, ok := topicMap["Text"].(string); ok {
- result.WriteString(fmt.Sprintf(" β’ %s\n", text))
- }
- }
- }
- }
-
- finalResult := result.String()
- ch <- finalResult
- return finalResult, nil
+ return fmt.Sprintf("Git status:\n%s", string(output)), nil
}
-// test_runner: Detect and run tests in the project
-func (d *Del) testRunner(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
- testType, _ := args["type"].(string)
- path, _ := args["path"].(string)
- if path == "" {
- path = "."
- }
-
- var cmd *exec.Cmd
- var output strings.Builder
-
- // Auto-detect test framework if not specified
- if testType == "" {
- output.WriteString("π Detecting test framework...\n")
-
- // Check for Go tests
- if _, err := os.Stat("go.mod"); err == nil {
- testType = "go"
- output.WriteString("β
Found Go project\n")
- }
- // Check for Node.js tests
- if _, err := os.Stat("package.json"); err == nil {
- if testType != "" {
- testType = "multi"
- } else {
- testType = "node"
- output.WriteString("β
Found Node.js project\n")
- }
- }
- // Check for Python tests
- if _, err := os.Stat("requirements.txt"); err == nil {
- if testType != "" {
- testType = "multi"
- } else {
- testType = "python"
- output.WriteString("β
Found Python project\n")
- }
- } else if _, err := os.Stat("pyproject.toml"); err == nil {
- if testType != "" {
- testType = "multi"
- } else {
- testType = "python"
- output.WriteString("β
Found Python project\n")
- }
- }
- // Check for Rust tests
- if _, err := os.Stat("Cargo.toml"); err == nil {
- if testType != "" {
- testType = "multi"
- } else {
- testType = "rust"
- output.WriteString("β
Found Rust project\n")
- }
- }
- }
-
- output.WriteString(fmt.Sprintf("π Running %s tests...\n\n", testType))
-
- switch testType {
- case "go":
- cmd = exec.CommandContext(ctx, "go", "test", "./...")
-
- case "node", "npm":
- cmd = exec.CommandContext(ctx, "npm", "test")
-
- case "yarn":
- cmd = exec.CommandContext(ctx, "yarn", "test")
-
- case "python", "pytest":
- cmd = exec.CommandContext(ctx, "python", "-m", "pytest", path)
-
- case "python-unittest":
- cmd = exec.CommandContext(ctx, "python", "-m", "unittest", "discover", path)
-
- case "rust", "cargo":
- cmd = exec.CommandContext(ctx, "cargo", "test")
-
- case "make":
- cmd = exec.CommandContext(ctx, "make", "test")
-
- case "multi":
- // For multi-language projects, try the most common
- output.WriteString("π Multi-language project detected. Trying common test commands:\n\n")
-
- // Try Go first
- if _, err := os.Stat("go.mod"); err == nil {
- output.WriteString("πΉ Running Go tests:\n")
- goCmd := exec.CommandContext(ctx, "go", "test", "./...")
- if goOut, err := goCmd.CombinedOutput(); err == nil {
- output.WriteString(string(goOut))
- output.WriteString("\n")
- }
- }
-
- // Try npm if package.json exists
- if _, err := os.Stat("package.json"); err == nil {
- output.WriteString("π¦ Running npm tests:\n")
- npmCmd := exec.CommandContext(ctx, "npm", "test")
- if npmOut, err := npmCmd.CombinedOutput(); err == nil {
- output.WriteString(string(npmOut))
- }
- }
-
- result := output.String()
- ch <- result
- return result, nil
-
- default:
- return nil, fmt.Errorf("unsupported test type: %s. Supported: go, node, python, rust, make", testType)
+func (d *Del) writeFile(ctx context.Context, args map[string]interface{}) (string, error) {
+ path, ok1 := args["path"].(string)
+ content, ok2 := args["content"].(string)
+ if !ok1 || !ok2 {
+ return "", fmt.Errorf("missing 'path' or 'content' argument")
}
- testOutput, err := cmd.CombinedOutput()
- result := output.String() + string(testOutput)
-
+ err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
- result += fmt.Sprintf("\nβ Tests failed with error: %s", err)
- } else {
- result += "\nβ
Tests completed successfully!"
+ return "", err
}
- ch <- result
- return result, nil
+ return fmt.Sprintf("Wrote %d bytes to %s", len(content), path), nil
}
-// multi_edit: Edit multiple files with find-replace operations
-func (d *Del) multiEdit(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
- operations, ok := args["operations"].([]interface{})
- if !ok {
- return nil, fmt.Errorf("missing 'operations' array argument")
- }
-
- var results []string
- var output strings.Builder
-
- output.WriteString("π§ Multi-file editing:\n\n")
-
- for i, op := range operations {
- opMap, ok := op.(map[string]interface{})
- if !ok {
- return nil, fmt.Errorf("operation %d is not a valid object", i)
- }
-
- filePath, _ := opMap["file"].(string)
- oldText, _ := opMap["old"].(string)
- newText, _ := opMap["new"].(string)
-
- if filePath == "" || oldText == "" {
- return nil, fmt.Errorf("operation %d missing required 'file' or 'old' fields", i)
- }
-
- // Read the file
- data, err := os.ReadFile(filePath)
- if err != nil {
- msg := fmt.Sprintf("β %s: failed to read (%v)\n", filePath, err)
- output.WriteString(msg)
- results = append(results, msg)
- continue
- }
-
- content := string(data)
-
- // Check if old text exists
- if !strings.Contains(content, oldText) {
- msg := fmt.Sprintf("β οΈ %s: text not found\n", filePath)
- output.WriteString(msg)
- results = append(results, msg)
- continue
- }
-
- // Perform replacement
- newContent := strings.ReplaceAll(content, oldText, newText)
-
- // Write back to file
- if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
- msg := fmt.Sprintf("β %s: failed to write (%v)\n", filePath, err)
- output.WriteString(msg)
- results = append(results, msg)
- continue
- }
-
- msg := fmt.Sprintf("β
%s: updated successfully\n", filePath)
- output.WriteString(msg)
- results = append(results, msg)
- }
-
- result := output.String()
- ch <- result
- return result, nil
-}
-
-// batch_operations: Execute multiple file operations (read, write, list) in sequence
-func (d *Del) batchOperations(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
- operations, ok := args["operations"].([]interface{})
+func (d *Del) analyzeCode(ctx context.Context, args map[string]interface{}) (string, error) {
+ content, ok := args["content"].(string)
if !ok {
- return nil, fmt.Errorf("missing 'operations' array argument")
- }
-
- var output strings.Builder
- output.WriteString("π Batch operations:\n\n")
-
- for i, op := range operations {
- opMap, ok := op.(map[string]interface{})
+ path, ok := args["path"].(string)
if !ok {
- return nil, fmt.Errorf("operation %d is not a valid object", i)
- }
-
- opType, _ := opMap["type"].(string)
- path, _ := opMap["path"].(string)
-
- switch opType {
- case "read":
- if path == "" {
- return nil, fmt.Errorf("operation %d: read requires 'path'", i)
- }
- data, err := os.ReadFile(path)
- if err != nil {
- output.WriteString(fmt.Sprintf("β Read %s: %v\n", path, err))
- } else {
- output.WriteString(fmt.Sprintf("π Read %s (%d bytes):\n%s\n\n", path, len(data), string(data)))
- }
-
- case "list":
- if path == "" {
- path = "."
- }
- entries, err := os.ReadDir(path)
- if err != nil {
- output.WriteString(fmt.Sprintf("β List %s: %v\n", path, err))
- } else {
- output.WriteString(fmt.Sprintf("π List %s:\n", path))
- for _, entry := range entries {
- prefix := "π"
- if entry.IsDir() {
- prefix = "π"
- }
- output.WriteString(fmt.Sprintf(" %s %s\n", prefix, entry.Name()))
- }
- output.WriteString("\n")
- }
-
- case "write":
- content, _ := opMap["content"].(string)
- if path == "" || content == "" {
- return nil, fmt.Errorf("operation %d: write requires 'path' and 'content'", i)
- }
- if err := os.WriteFile(path, []byte(content), 0644); err != nil {
- output.WriteString(fmt.Sprintf("β Write %s: %v\n", path, err))
- } else {
- output.WriteString(fmt.Sprintf("β
Write %s (%d bytes)\n", path, len(content)))
- }
-
- default:
- return nil, fmt.Errorf("operation %d: unsupported type '%s'", i, opType)
- }
- }
-
- result := output.String()
- ch <- result
- return result, nil
-}
-
-// requiresConfirmation checks if an operation requires user confirmation
-func (d *Del) requiresConfirmation(operation string, args map[string]interface{}) bool {
- if !d.safeMode {
- return false
- }
-
- // Define sensitive operations
- sensitiveOps := map[string]bool{
- "write_file": true,
- "multi_edit": true,
- "git_push": true,
- "git_commit": true,
- "run_command": true,
- "format_code": true,
- }
-
- // Check for sensitive git operations
- if operation == "git_operation" {
- if gitOp, ok := args["operation"].(string); ok {
- return gitOp == "push" || gitOp == "commit" || gitOp == "reset" || gitOp == "clean"
- }
- }
-
- // Check for dangerous shell commands
- if operation == "run_command" {
- if cmd, ok := args["command"].(string); ok {
- dangerousCommands := []string{"rm", "sudo", "mv", "cp", "chmod", "chown", "dd", "mkfs"}
- cmdLower := strings.ToLower(cmd)
- for _, dangerous := range dangerousCommands {
- if strings.Contains(cmdLower, dangerous) {
- return true
- }
- }
+ return "", fmt.Errorf("missing 'content' or 'path' argument")
}
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return "", err
+ }
+ content = string(data)
}
- return sensitiveOps[operation]
+ lines := strings.Count(content, "\n") + 1
+ funcs := regexp.MustCompile(`(?m)^[ \t]*(func |def |function )`).FindAllStringIndex(content, -1)
+
+ return fmt.Sprintf("Code analysis:\nLines: %d\nFunctions: %d", lines, len(funcs)), nil
}
-// askConfirmation prompts user for confirmation
-func (d *Del) askConfirmation(operation string, description string) bool {
- actionKey := fmt.Sprintf("%s:%s", operation, description)
-
- // Check if already confirmed for this session
- if confirmed, exists := d.confirmedActions[actionKey]; exists && confirmed {
- return true
+func (d *Del) searchCode(ctx context.Context, args map[string]interface{}) (string, error) {
+ pattern, ok := args["pattern"].(string)
+ if !ok {
+ return "", fmt.Errorf("missing 'pattern' argument")
}
- fmt.Printf("\nβ οΈ CONFIRMATION REQUIRED β οΈ\n")
- fmt.Printf("Operation: %s\n", operation)
- fmt.Printf("Description: %s\n", description)
- fmt.Printf("Continue? (y/N): ")
-
- var response string
- fmt.Scanln(&response)
+ path, ok := args["path"].(string)
+ if !ok {
+ path = "."
+ }
- confirmed := strings.ToLower(strings.TrimSpace(response)) == "y"
- d.confirmedActions[actionKey] = confirmed
+ cmd := exec.CommandContext(ctx, "grep", "-r", pattern, path)
+ output, err := cmd.CombinedOutput()
- return confirmed
-}
-
-// formatArgs formats tool arguments for display
-func (d *Del) formatArgs(args map[string]interface{}) string {
- if len(args) == 0 {
- return ""
+ if err != nil && len(output) == 0 {
+ return fmt.Sprintf("No matches found for pattern: %s", pattern), nil
}
- var parts []string
- for key, value := range args {
- switch v := value.(type) {
- case string:
- if len(v) > 30 {
- parts = append(parts, fmt.Sprintf("%s: \"%.30s...\"", key, v))
- } else {
- parts = append(parts, fmt.Sprintf("%s: \"%s\"", key, v))
- }
- default:
- parts = append(parts, fmt.Sprintf("%s: %v", key, v))
- }
- }
- return strings.Join(parts, ", ")
-}
-
-// summarizeResult creates a brief summary of tool results
-func (d *Del) summarizeResult(result interface{}) string {
- switch v := result.(type) {
- case string:
- lines := strings.Split(v, "\n")
- if len(lines) == 1 && len(v) < 50 {
- return v
- } else if len(lines) > 1 {
- return fmt.Sprintf("Output: %d lines", len(lines))
- } else {
- return fmt.Sprintf("Output: %d chars", len(v))
- }
- default:
- return "Completed successfully"
- }
+ return fmt.Sprintf("Search results for '%s':\n%s", pattern, string(output)), nil
}
-// parseChineseFormat handles Chinese model format: <ο½toolβcallsβbeginο½>...
-func (d *Del) parseChineseFormat(response string) []ToolCall {
+// Parse Chinese model tool calls
+func (d *Del) parseToolCalls(response string) []ToolCall {
var calls []ToolCall
- // Simple pattern to match Chinese model tool calls
- // This handles: function<ο½toolβsepο½>name {...}
+ // Look for function<ο½toolβsepο½>name pattern
re := regexp.MustCompile(`function<ο½toolβsepο½>(\w+)`)
matches := re.FindAllStringSubmatch(response, -1)
@@ -1042,17 +220,30 @@ func (d *Del) parseChineseFormat(response string) []ToolCall {
toolName := match[1]
// Look for JSON args after the function name
- // Find the position after this match
startPos := strings.Index(response, match[0]) + len(match[0])
remaining := response[startPos:]
- // Look for JSON object - simple approach
+ // Find JSON object
var args map[string]interface{}
jsonStart := strings.Index(remaining, "{")
if jsonStart != -1 {
- jsonEnd := strings.Index(remaining[jsonStart:], "}")
+ // Find matching closing brace
+ braceCount := 0
+ jsonEnd := -1
+ for i, char := range remaining[jsonStart:] {
+ if char == '{' {
+ braceCount++
+ } else if char == '}' {
+ braceCount--
+ if braceCount == 0 {
+ jsonEnd = jsonStart + i + 1
+ break
+ }
+ }
+ }
+
if jsonEnd != -1 {
- jsonStr := remaining[jsonStart : jsonStart+jsonEnd+1]
+ jsonStr := remaining[jsonStart:jsonEnd]
json.Unmarshal([]byte(jsonStr), &args)
}
}
@@ -1065,542 +256,139 @@ func (d *Del) parseChineseFormat(response string) []ToolCall {
}
}
- // Fallback: try simpler regex pattern
- if len(calls) == 0 {
- re = regexp.MustCompile(`(?s)<ο½toolβcallβbeginο½>.*?function<ο½toolβsepο½>(\w+)\s*(\{[^}]*\}?)`)
- matches = re.FindAllStringSubmatch(response, -1)
-
- for _, match := range matches {
- if len(match) >= 3 {
- toolName := match[1]
- argsJSON := strings.TrimSpace(match[2])
-
- var args map[string]interface{}
- if argsJSON == "" || argsJSON == "{}" {
- args = make(map[string]interface{})
- } else {
- json.Unmarshal([]byte(argsJSON), &args)
- if args == nil {
- args = make(map[string]interface{})
- }
- }
-
- calls = append(calls, ToolCall{Name: toolName, Args: args})
- }
- }
- }
-
return calls
}
-// parseOpenAIXMLFormat handles OpenAI XML format: <tool_calls><invoke>...
-func (d *Del) parseOpenAIXMLFormat(response string) []ToolCall {
- var calls []ToolCall
-
- // Pattern to match OpenAI XML tool calls
- re := regexp.MustCompile(`(?s)<tool_calls>.*?<invoke name="(\w+)">(.*?)</invoke>.*?</tool_calls>`)
- matches := re.FindAllStringSubmatch(response, -1)
-
- for _, match := range matches {
- if len(match) >= 3 {
- toolName := match[1]
- paramsXML := match[2]
-
- // Parse parameters from XML
- args := make(map[string]interface{})
- paramRe := regexp.MustCompile(`<parameter name="([^"]+)">([^<]*)</parameter>`)
- paramMatches := paramRe.FindAllStringSubmatch(paramsXML, -1)
-
- for _, paramMatch := range paramMatches {
- if len(paramMatch) >= 3 {
- args[paramMatch[1]] = paramMatch[2]
- }
- }
-
- calls = append(calls, ToolCall{Name: toolName, Args: args})
+func (d *Del) executeTool(ctx context.Context, call ToolCall) string {
+ fmt.Printf("\nβ %s", call.Name)
+ if len(call.Args) > 0 {
+ argsStr := d.formatArgs(call.Args)
+ if argsStr != "" {
+ fmt.Printf("(%s)", argsStr)
}
}
+ fmt.Print("\n")
- return calls
-}
-
-// parseOpenAIJSONFormat handles OpenAI JSON format: [{"name": "...", "parameters": {...}}]
-func (d *Del) parseOpenAIJSONFormat(response string) []ToolCall {
- var calls []ToolCall
-
- // Look for JSON array of tool calls
- re := regexp.MustCompile(`(?s)\[(\{[^}]*"name"[^}]*\}(?:,\s*\{[^}]*"name"[^}]*\})*)\]`)
- matches := re.FindAllStringSubmatch(response, -1)
-
- for _, match := range matches {
- if len(match) >= 2 {
- jsonStr := "[" + match[1] + "]"
-
- var toolCalls []map[string]interface{}
- if err := json.Unmarshal([]byte(jsonStr), &toolCalls); err != nil {
- continue
- }
-
- for _, tc := range toolCalls {
- if name, ok := tc["name"].(string); ok {
- args := make(map[string]interface{})
- if params, ok := tc["parameters"].(map[string]interface{}); ok {
- args = params
- }
- calls = append(calls, ToolCall{Name: name, Args: args})
- }
- }
- }
+ tool, exists := d.tools[call.Name]
+ if !exists {
+ result := fmt.Sprintf(" βΏ β Unknown tool: %s", call.Name)
+ fmt.Println(result)
+ return result
}
- return calls
-}
-
-// parseLlamaFormat handles Llama format: <function=name>args</function>
-func (d *Del) parseLlamaFormat(response string) []ToolCall {
- var calls []ToolCall
-
- // Pattern to match Llama function calls
- re := regexp.MustCompile(`(?s)<function=(\w+)>(\{[^}]*\}?)</function>`)
- matches := re.FindAllStringSubmatch(response, -1)
+ result, err := tool(ctx, call.Args)
+ if err != nil {
+ errorMsg := fmt.Sprintf(" βΏ β Error: %s", err.Error())
+ fmt.Println(errorMsg)
+ return errorMsg
+ }
- for _, match := range matches {
- if len(match) >= 3 {
- toolName := match[1]
- argsJSON := match[2]
-
- var args map[string]interface{}
- if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
- continue // Skip invalid JSON
- }
-
- calls = append(calls, ToolCall{Name: toolName, Args: args})
- }
+ // Show the result with Claude Code style formatting
+ lines := strings.Split(result, "\n")
+ if len(lines) <= 5 {
+ fmt.Printf(" βΏ %s\n", strings.ReplaceAll(result, "\n", "\n "))
+ } else {
+ fmt.Printf(" βΏ %s (%d lines, ctrl+r to expand)\n", strings.Split(result, "\n")[0], len(lines))
}
- return calls
+ return result
}
-// parseCustomFormat handles Del's legacy format: TOOL_USE: name args
-func (d *Del) parseCustomFormat(response string) []ToolCall {
- var calls []ToolCall
-
- // Pattern to match custom TOOL_USE format
- re := regexp.MustCompile(`(?s)TOOL_USE:\s*(\w+)\s*(\{.*?\})`)
- matches := re.FindAllStringSubmatch(response, -1)
+func (d *Del) formatArgs(args map[string]interface{}) string {
+ if len(args) == 0 {
+ return ""
+ }
- for _, match := range matches {
- if len(match) >= 3 {
- toolName := match[1]
- argsJSON := match[2]
-
- var args map[string]interface{}
- if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
- continue // Skip invalid JSON
- }
-
- calls = append(calls, ToolCall{Name: toolName, Args: args})
+ var parts []string
+ for key, value := range args {
+ if str, ok := value.(string); ok && len(str) > 30 {
+ parts = append(parts, fmt.Sprintf("%s: \"%.30s...\"", key, str))
+ } else {
+ parts = append(parts, fmt.Sprintf("%s: %v", key, value))
}
}
-
- return calls
+ return strings.Join(parts, ", ")
}
-// parseMarkdownFormat handles markdown function calls: ## Function Call\nname(args)
-func (d *Del) parseMarkdownFormat(response string) []ToolCall {
- var calls []ToolCall
-
- // Pattern to match markdown function calls
- re := regexp.MustCompile(`(?s)##\s*(?:Function Call|Tool Call)[\s\S]*?(\w+)\s*\((\{[^}]*\}?)\)`)
- matches := re.FindAllStringSubmatch(response, -1)
-
- for _, match := range matches {
- if len(match) >= 3 {
- toolName := match[1]
- argsJSON := match[2]
-
- var args map[string]interface{}
- if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
- continue // Skip invalid JSON
- }
-
- calls = append(calls, ToolCall{Name: toolName, Args: args})
+func (d *Del) streamResponse(ctx context.Context, response string) {
+ // Simulate streaming by printing word by word
+ words := strings.Fields(response)
+ for i, word := range words {
+ fmt.Print(word)
+ if i < len(words)-1 {
+ fmt.Print(" ")
}
+ time.Sleep(30 * time.Millisecond)
}
-
- return calls
}
-// parseToolCalls attempts to parse tool calls from response using all known formats
-func (d *Del) parseToolCalls(response string) []ToolCall {
- // Try all known formats in order of likelihood
-
- // 1. Chinese models (DeepSeek, Qwen, etc.) - most common currently
- if calls := d.parseChineseFormat(response); len(calls) > 0 {
- return calls
- }
-
- // 2. OpenAI XML format
- if calls := d.parseOpenAIXMLFormat(response); len(calls) > 0 {
- return calls
- }
-
- // 3. OpenAI JSON format
- if calls := d.parseOpenAIJSONFormat(response); len(calls) > 0 {
- return calls
- }
-
- // 4. Llama format
- if calls := d.parseLlamaFormat(response); len(calls) > 0 {
- return calls
- }
+func (d *Del) processMessage(ctx context.Context, userInput string) {
+ // Add user message to history
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: userInput})
- // 5. Markdown format
- if calls := d.parseMarkdownFormat(response); len(calls) > 0 {
- return calls
- }
+ // Get response from model
+ var fullResponse string
+ err := d.client.Chat(ctx, &api.ChatRequest{
+ Model: d.model,
+ Messages: d.chatHistory,
+ }, func(resp api.ChatResponse) error {
+ fullResponse += resp.Message.Content
+ return nil
+ })
- // 6. Legacy custom format (for backward compatibility)
- if calls := d.parseCustomFormat(response); len(calls) > 0 {
- return calls
+ if err != nil {
+ fmt.Printf("β Error: %v\n", err)
+ return
}
- return []ToolCall{}
-}
-
-// 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 {
- // Get current working directory for context
- cwd, _ := os.Getwd()
-
- // Enhanced system prompt with current directory context
- contextualPrompt := SystemPrompt + fmt.Sprintf(`
-
-# CURRENT SESSION CONTEXT
-
-ποΈ **Working Directory**: %s
-π **Session Info**: You are currently running in this directory. When users ask about "this project", "current directory", "here", or "analyze the code", they mean this location.
-
-# COMMON USER PATTERNS
-
-When users say:
-- "analyze the code" β Use project_context first, then analyze key files
-- "scan for vulnerabilities" β Use security_scan {"path": "."}
-- "what's in this directory" β Use list_dir {"path": "."}
-- "read the main file" β Look for main.go, main.py, index.js, etc.
-- "show me the project structure" β Use project_context {}
-
-Always assume "." (current directory) when no path is specified for file operations.`, cwd)
-
- d := &Del{
- aiProvider: provider,
- tools: make(map[string]*Tool),
- safeMode: true, // Enable safe mode by default
- confirmedActions: make(map[string]bool),
- chatHistory: []api.Message{
- {
- Role: "system",
- Content: contextualPrompt,
- },
- },
- }
- d.tools["run_command"] = &Tool{
- Name: "run_command",
- Description: "Execute a shell command and return the output",
- Handler: d.runCommand,
- }
- d.tools["read_file"] = &Tool{
- Name: "read_file",
- Description: "Reads the contents of a file",
- Handler: d.readFile,
- }
- d.tools["write_file"] = &Tool{
- Name: "write_file",
- Description: "Writes content to a file",
- 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: 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["security_scan"] = &Tool{
- Name: "security_scan",
- Description: "Scan code for security vulnerabilities using gosec and semgrep",
- Handler: d.securityScan,
- }
- d.tools["project_context"] = &Tool{
- Name: "project_context",
- Description: "Analyze current project structure and provide development context",
- Handler: d.projectContext,
- }
- d.tools["git_operation"] = &Tool{
- Name: "git_operation",
- Description: "Execute git operations (status, add, commit, push, pull, diff, log, branch, etc)",
- Handler: d.gitOperation,
- }
- d.tools["web_search"] = &Tool{
- Name: "web_search",
- Description: "Search the web for information and documentation",
- Handler: d.webSearch,
- }
- d.tools["test_runner"] = &Tool{
- Name: "test_runner",
- Description: "Detect and run tests for various languages (Go, Node.js, Python, Rust)",
- Handler: d.testRunner,
- }
- d.tools["multi_edit"] = &Tool{
- Name: "multi_edit",
- Description: "Edit multiple files with find-replace operations",
- Handler: d.multiEdit,
- }
- d.tools["batch_operations"] = &Tool{
- Name: "batch_operations",
- Description: "Execute multiple file operations (read, write, list) in sequence",
- Handler: d.batchOperations,
- }
- d.tools["mcp_call"] = &Tool{
- Name: "mcp_call",
- Description: "Stub for MCP integration",
- Handler: d.stubMCP,
- }
- return d
-}
-
-func (d *Del) streamChat(ctx context.Context, history []api.Message) (string, error) {
- return d.aiProvider.StreamChat(ctx, history)
-}
-
-func (d *Del) executeToolCalls(ctx context.Context, toolCalls []ToolCall) map[string]interface{} {
- results := make(map[string]interface{})
+ // Add assistant response to history
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: fullResponse})
- fmt.Printf("\nβ Executing %d tool call(s):\n", len(toolCalls))
+ // Check for tool calls
+ toolCalls := d.parseToolCalls(fullResponse)
- for i, toolCall := range toolCalls {
- fmt.Printf("\nβ %s(%s)\n", toolCall.Name, d.formatArgs(toolCall.Args))
- tool, ok := d.tools[toolCall.Name]
- if !ok {
- fmt.Printf(" βΏ β Unknown tool: %s\n", toolCall.Name)
- results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
- "tool": toolCall.Name,
- "error": fmt.Sprintf("unknown tool: %s", toolCall.Name),
- }
- continue
- }
-
- // Check if confirmation is required
- if d.requiresConfirmation(toolCall.Name, toolCall.Args) {
- argsJSON, _ := json.Marshal(toolCall.Args)
- description := fmt.Sprintf("%s with args: %s", toolCall.Name, string(argsJSON))
- if !d.askConfirmation(toolCall.Name, description) {
- fmt.Printf(" βΏ π« Operation cancelled by user\n")
- results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
- "tool": toolCall.Name,
- "error": "operation cancelled by user",
- }
- continue
- }
+ if len(toolCalls) > 0 {
+ // Execute tools with Claude Code style progress
+ var toolResults []string
+ for _, call := range toolCalls {
+ result := d.executeTool(ctx, call)
+ toolResults = append(toolResults, fmt.Sprintf("%s: %s", call.Name, result))
}
- fmt.Printf(" βΏ ")
- ch := make(chan string, 10)
- var output strings.Builder
- go func() {
- for line := range ch {
- fmt.Print(line)
- output.WriteString(line)
- }
- }()
+ // Get final response with tool results
+ toolResultsMsg := fmt.Sprintf("Tool results:\n%s\n\nProvide a natural response based on these results.",
+ strings.Join(toolResults, "\n"))
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: toolResultsMsg})
- result, err := tool.Handler(ctx, toolCall.Args, ch)
- close(ch)
+ var finalResponse string
+ err = d.client.Chat(ctx, &api.ChatRequest{
+ Model: d.model,
+ Messages: d.chatHistory,
+ }, func(resp api.ChatResponse) error {
+ finalResponse += resp.Message.Content
+ return nil
+ })
- if err != nil {
- fmt.Printf(" βΏ β Error: %s\n", err.Error())
- results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
- "tool": toolCall.Name,
- "error": err.Error(),
- }
- } else {
- // Show success with brief result summary
- resultSummary := d.summarizeResult(result)
- fmt.Printf(" βΏ β
%s\n", resultSummary)
- results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
- "tool": toolCall.Name,
- "result": result,
- }
- }
- }
-
- return results
-}
-
-func (d *Del) handleToolCalls(ctx context.Context, response string) (bool, map[string]interface{}) {
- // Use universal parser to detect tool calls in any format
- toolCalls := d.parseToolCalls(response)
- if len(toolCalls) == 0 {
- return false, nil // No tool calls found
- }
-
- // Show parsing success
- fmt.Printf("\nβ Detected %d tool call(s) using universal parser\n", len(toolCalls))
-
- // Execute all tool calls and return results
- results := d.executeToolCalls(ctx, toolCalls)
- return true, results
-}
-
-func (d *Del) showHelp() {
- fmt.Println(`
-π€ Del the Funky Robosapien - Your Ultimate Coding Assistant
-
-π Quick Commands:
- help, h Show this help
- quit, q, exit Exit Del
- clear, cls Clear screen
- context, ctx Show project context
- tools List all available tools
- safe Toggle safe mode (confirmation prompts)
- unsafe Disable safe mode (DANGEROUS)
-
-π₯ Power User Tips:
- Just talk naturally! Del understands:
- β’ "scan this project for vulnerabilities"
- β’ "read and explain main.go"
- β’ "what functions are in utils.py?"
- β’ "check git status and commit changes"
- β’ "run the tests"
- β’ "search for golang best practices"
- β’ "analyze the code quality"
-
-π§ Available Tools:
- β File Operations β’ read, write, list files
- β Multi-File Editing β’ batch edits across files
- β Code Analysis β’ analyze, format, lint code
- β Security Scanning β’ find vulnerabilities
- β Git Operations β’ status, commit, push, diff, log
- β Web Search β’ search for documentation and info
- β Test Running β’ auto-detect and run project tests
- β Command Execution β’ run any shell command
- β Project Understanding β’ context-aware assistance
- π‘οΈ Safe Mode β’ confirmation prompts for dangerous ops
-
-π― Pro Tip: Del learns your project as you work!
-`)
-}
-
-func (d *Del) clearScreen() {
- fmt.Print("\033[2J\033[H")
-}
-
-func (d *Del) showContext() {
- cwd, _ := os.Getwd()
- safeStatus := "π‘οΈ ENABLED"
- if !d.safeMode {
- safeStatus = "β οΈ DISABLED"
- }
- fmt.Printf(`
-πΌ Current Project Context:
- π Directory: %s
- π€ Model: %s
- π¬ Chat History: %d messages
- π‘οΈ Safe Mode: %s
-`, cwd, d.aiProvider.Name(), len(d.chatHistory), safeStatus)
-
- // Show project type detection
- projectTypes := map[string]string{
- "go.mod": "πΉ Go project",
- "package.json": "π¦ Node.js project",
- "Cargo.toml": "π¦ Rust project",
- "requirements.txt": "π Python project",
- "pom.xml": "β Java project",
- }
-
- fmt.Println(" π Project Type:")
- found := false
- for file, desc := range projectTypes {
- if _, err := os.Stat(file); err == nil {
- fmt.Printf(" %s\n", desc)
- found = true
- }
- }
- if !found {
- fmt.Println(" π Generic project")
- }
-
- // Show recent files if any
- if entries, err := os.ReadDir("."); err == nil {
- fmt.Println(" π Key Files:")
- count := 0
- for _, entry := range entries {
- if !entry.IsDir() && isImportantFile(entry.Name()) && count < 8 {
- fmt.Printf(" β’ %s\n", entry.Name())
- count++
- }
+ if err == nil {
+ fmt.Print("\nπ€ Del: ")
+ d.streamResponse(ctx, finalResponse)
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
}
+ } else {
+ // No tools, just stream the response
+ fmt.Print("π€ Del: ")
+ d.streamResponse(ctx, fullResponse)
}
- fmt.Println("\nπ‘ Del knows this is your working directory.")
- fmt.Println(" Just say 'analyze the code' or 'scan for vulnerabilities'!")
- fmt.Println()
-}
-
-func (d *Del) showTools() {
- fmt.Println(`
-π§ Available Tools:
-`)
- for name, tool := range d.tools {
- fmt.Printf(" β %-18s %s\n", name, tool.Description)
- }
fmt.Println()
}
-func (d *Del) StartREPL(ctx context.Context) {
- d.clearScreen()
+func (d *Del) Start(ctx context.Context) {
cwd, _ := os.Getwd()
- fmt.Printf("π€ Del the Funky Robosapien is ready!\n")
- fmt.Printf("π€ Powered by %s\n", d.aiProvider.Name())
+ fmt.Println("π€ Del the Funky Robosapien (Claude Code Style)")
+ fmt.Printf("π€ Powered by Ollama (%s)\n", d.model)
fmt.Printf("π Working in: %s\n", cwd)
- fmt.Println("π Type 'help' for commands, 'quit' to exit, or just ask me anything!")
- fmt.Println("π‘ Try: 'analyze the code', 'scan for vulnerabilities', 'show project structure'")
+ fmt.Println("π‘ Try: 'list files', 'read main.go', 'check git status', 'analyze the code'")
fmt.Println()
scanner := bufio.NewScanner(os.Stdin)
@@ -1609,92 +397,22 @@ func (d *Del) StartREPL(ctx context.Context) {
if !scanner.Scan() {
break
}
+
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
- // Handle quick commands
- switch strings.ToLower(input) {
- case "quit", "q", "exit":
- fmt.Println("π Stay funky, keep coding!")
- return
- case "help", "h":
- d.showHelp()
- continue
- case "clear", "cls":
- d.clearScreen()
- continue
- case "context", "ctx":
- d.showContext()
- continue
- case "tools":
- d.showTools()
- continue
- case "safe":
- d.safeMode = !d.safeMode
- if d.safeMode {
- fmt.Println("π‘οΈ Safe mode ENABLED - dangerous operations will require confirmation")
- } else {
- fmt.Println("β οΈ Safe mode DISABLED - dangerous operations will execute without confirmation")
- }
- continue
- case "unsafe":
- d.safeMode = false
- fmt.Println("β οΈ Safe mode DISABLED - BE CAREFUL!")
- continue
- }
-
- fmt.Print("π€ Del: ")
- d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: input})
-
- // Stage 1: Get initial response from model (should contain tool calls)
- response, err := d.aiProvider.Chat(ctx, d.chatHistory)
- if err != nil {
- fmt.Printf("\nβ οΈ Error: %s\n\n", err)
- continue
- }
-
- // Check if there are tool calls to execute
- hasTools, toolResults := d.handleToolCalls(ctx, response)
-
- if hasTools {
- // Stage 2: Tool calls were found and executed, ask model for final response
- d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
-
- fmt.Printf("\nβ Providing tool results to model for final response...\n")
-
- // Create tool results message
- toolResultsJSON, _ := json.MarshalIndent(toolResults, "", " ")
- toolMessage := fmt.Sprintf("Tool execution results:\n```json\n%s\n```\n\nPlease provide a natural response based on these results.", string(toolResultsJSON))
- d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: toolMessage})
-
- // Get final response with tool results
- finalResponse, err := d.aiProvider.Chat(ctx, d.chatHistory)
- if err != nil {
- fmt.Printf(" βΏ β Error getting final response: %s\n\n", err)
- continue
- }
- fmt.Printf(" βΏ β
Generated natural response\n")
- fmt.Printf("\nπ€ Del: %s", finalResponse)
- d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
- } else {
- // No tool calls, just display the response normally
- fmt.Print(response)
- d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
+ if input == "quit" || input == "exit" || input == "q" {
+ fmt.Println("π Stay funky!")
+ break
}
+ d.processMessage(ctx, input)
fmt.Println()
}
}
-func createOllamaProvider(model string) *OllamaProvider {
- if model == "" {
- model = "deepseek-coder-v2:16b"
- }
- return NewOllamaProvider(model)
-}
-
func main() {
var model = flag.String("model", "deepseek-coder-v2:16b", "Ollama model to use")
var help = flag.Bool("help", false, "Show help message")
@@ -1702,7 +420,7 @@ func main() {
flag.Parse()
if *help {
- fmt.Println(`π€ Del the Funky Robosapien - Ultimate AI Coding Assistant
+ fmt.Println(`π€ Del the Funky Robosapien - Claude Code Style AI Assistant
Usage:
del [flags]
@@ -1713,29 +431,20 @@ Flags:
Popular Models:
deepseek-coder-v2:16b # Best for coding (default)
- codellama:34b # Meta's coding model
- codegemma:7b # Google's coding model
- qwen2.5-coder:32b # Alibaba's coding model
+ codellama:34b # Meta's coding model
+ qwen2.5-coder:32b # Alibaba's coding model
Examples:
del # Use default model
del --model codellama:34b # Use CodeLlama
- del --model qwen2.5-coder:32b # Use Qwen Coder
-Interactive Commands:
- help, h Show available commands and tools
- quit, q, exit Exit Del
- clear, cls Clear screen
- context Show current project context
-
-Del automatically uses tools based on your requests:
- "scan for vulnerabilities", "read main.go", "analyze this code", etc.
+Del automatically uses tools based on your requests with Claude Code style progress indicators:
+ "list files", "read main.go", "check git status", "run ls -la", "analyze the code"
`)
return
}
ctx := context.Background()
- provider := createOllamaProvider(*model)
- assistant := NewDel(provider)
- assistant.StartREPL(ctx)
-}
+ assistant := NewDel(*model)
+ assistant.Start(ctx)
+}
\ No newline at end of file
go.mod
@@ -2,10 +2,7 @@ module github.com/xlgmokha/deltron
go 1.24.0
-require (
- github.com/creack/pty v1.1.24
- github.com/ollama/ollama v0.9.2
-)
+require github.com/ollama/ollama v0.9.2
require (
golang.org/x/crypto v0.36.0 // indirect
go.sum
@@ -1,5 +1,3 @@
-github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
-github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=