Skip to main content
Glama
provider_terminal.go8.64 kB
package aicoder import ( "bufio" "context" "fmt" "io" "os" "os/exec" "strings" "sync" ) // TerminalProvider implements the AIProvider interface for local terminal/shell execution type TerminalProvider struct { shell string workingDir string env []string mu sync.Mutex } // NewTerminalProvider creates a new Terminal provider func NewTerminalProvider(shell string) *TerminalProvider { if shell == "" { // Detect default shell shell = os.Getenv("SHELL") if shell == "" { shell = "/bin/bash" // Default to bash } } return &TerminalProvider{ shell: shell, workingDir: ".", env: os.Environ(), } } // Name returns the provider name func (p *TerminalProvider) Name() string { return "terminal" } // GetCapabilities returns Terminal's capabilities func (p *TerminalProvider) GetCapabilities() ProviderCapabilities { return ProviderCapabilities{ SupportsStreaming: true, MaxContextTokens: 1000000, // No real limit for terminal MaxOutputTokens: 1000000, // No real limit for terminal SupportedModels: []string{ "bash", "sh", "zsh", "fish", "python", "node", "ruby", }, } } // GenerateCode executes commands in the terminal and returns the output func (p *TerminalProvider) GenerateCode(ctx context.Context, prompt string, options GenerateOptions) (*GenerateResult, error) { p.mu.Lock() defer p.mu.Unlock() // The prompt is treated as a command or script to execute // First, let's see if it's a multi-line script or a single command lines := strings.Split(prompt, "\n") var cmd *exec.Cmd var output strings.Builder var summary string // Determine shell to use (options.Model can override default) shell := p.shell if options.Model != "" && options.Model != "terminal" { shell = options.Model } // If it's a single line, execute directly if len(lines) == 1 && !strings.Contains(prompt, ";") && !strings.Contains(prompt, "&&") { cmd = exec.CommandContext(ctx, shell, "-c", prompt) summary = fmt.Sprintf("Executed command: %s", prompt) } else { // Multi-line script - write to temp file and execute tmpFile, err := os.CreateTemp("", "aicoder-script-*.sh") if err != nil { return nil, fmt.Errorf("failed to create temp script: %w", err) } defer os.Remove(tmpFile.Name()) // Write script content if _, err := tmpFile.WriteString(prompt); err != nil { return nil, fmt.Errorf("failed to write script: %w", err) } tmpFile.Close() // Make executable os.Chmod(tmpFile.Name(), 0755) // Execute script cmd = exec.CommandContext(ctx, shell, tmpFile.Name()) summary = "Executed multi-line script" } // Set working directory if p.workingDir != "" && p.workingDir != "." { cmd.Dir = p.workingDir } // Set environment cmd.Env = p.env // Capture both stdout and stderr stdout, err := cmd.StdoutPipe() if err != nil { return nil, fmt.Errorf("failed to create stdout pipe: %w", err) } stderr, err := cmd.StderrPipe() if err != nil { return nil, fmt.Errorf("failed to create stderr pipe: %w", err) } // Start command if err := cmd.Start(); err != nil { return nil, fmt.Errorf("failed to start command: %w", err) } // Read output var wg sync.WaitGroup wg.Add(2) // Read stdout go func() { defer wg.Done() scanner := bufio.NewScanner(stdout) for scanner.Scan() { output.WriteString(scanner.Text() + "\n") } }() // Read stderr go func() { defer wg.Done() scanner := bufio.NewScanner(stderr) for scanner.Scan() { output.WriteString("[stderr] " + scanner.Text() + "\n") } }() // Wait for command to complete wg.Wait() err = cmd.Wait() // Build result result := &GenerateResult{ Code: output.String(), Summary: summary, TokensUsed: len(output.String()), // Approximate by character count Model: shell, FinishReason: "complete", } // Include exit code in summary if command failed if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { result.Summary = fmt.Sprintf("%s (exit code: %d)", summary, exitErr.ExitCode()) } else { result.Summary = fmt.Sprintf("%s (error: %v)", summary, err) } } return result, nil } // StreamGenerate executes commands with real-time streaming output func (p *TerminalProvider) StreamGenerate(ctx context.Context, prompt string, options GenerateOptions) (<-chan GenerateUpdate, error) { ch := make(chan GenerateUpdate, 100) go func() { defer close(ch) p.mu.Lock() defer p.mu.Unlock() // Determine shell to use shell := p.shell if options.Model != "" && options.Model != "terminal" { shell = options.Model } // Create command var cmd *exec.Cmd lines := strings.Split(prompt, "\n") if len(lines) == 1 && !strings.Contains(prompt, ";") && !strings.Contains(prompt, "&&") { cmd = exec.CommandContext(ctx, shell, "-c", prompt) } else { // Multi-line script tmpFile, err := os.CreateTemp("", "aicoder-script-*.sh") if err != nil { ch <- GenerateUpdate{Error: fmt.Errorf("failed to create temp script: %w", err)} return } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(prompt); err != nil { ch <- GenerateUpdate{Error: fmt.Errorf("failed to write script: %w", err)} return } tmpFile.Close() os.Chmod(tmpFile.Name(), 0755) cmd = exec.CommandContext(ctx, shell, tmpFile.Name()) } // Set working directory and environment if p.workingDir != "" && p.workingDir != "." { cmd.Dir = p.workingDir } cmd.Env = p.env // Create pipes stdout, err := cmd.StdoutPipe() if err != nil { ch <- GenerateUpdate{Error: fmt.Errorf("failed to create stdout pipe: %w", err)} return } stderr, err := cmd.StderrPipe() if err != nil { ch <- GenerateUpdate{Error: fmt.Errorf("failed to create stderr pipe: %w", err)} return } // Start command if err := cmd.Start(); err != nil { ch <- GenerateUpdate{Error: fmt.Errorf("failed to start command: %w", err)} return } // Stream output done := make(chan bool, 2) // Stream stdout go func() { reader := bufio.NewReader(stdout) for { line, err := reader.ReadString('\n') if err != nil { if err != io.EOF { select { case ch <- GenerateUpdate{Content: fmt.Sprintf("[error reading stdout: %v]\n", err)}: case <-ctx.Done(): return } } break } select { case ch <- GenerateUpdate{Content: line, TokensUsed: len(line)}: case <-ctx.Done(): return } } done <- true }() // Stream stderr go func() { reader := bufio.NewReader(stderr) for { line, err := reader.ReadString('\n') if err != nil { if err != io.EOF { select { case ch <- GenerateUpdate{Content: fmt.Sprintf("[error reading stderr: %v]\n", err)}: case <-ctx.Done(): return } } break } select { case ch <- GenerateUpdate{Content: "[stderr] " + line, TokensUsed: len(line)}: case <-ctx.Done(): return } } done <- true }() // Wait for streams to finish <-done <-done // Wait for command to complete if err := cmd.Wait(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { ch <- GenerateUpdate{ Content: fmt.Sprintf("\n[Process exited with code %d]\n", exitErr.ExitCode()), FinishReason: "error", } } else { ch <- GenerateUpdate{ Content: fmt.Sprintf("\n[Process error: %v]\n", err), FinishReason: "error", } } } else { ch <- GenerateUpdate{ Content: "\n[Process completed successfully]\n", FinishReason: "complete", } } }() return ch, nil } // ValidateConfig validates the provider configuration func (p *TerminalProvider) ValidateConfig(config ProviderConfig) error { // Terminal provider doesn't need API keys // Just validate the shell/model if specified if config.Model != "" { // Check if the shell exists if _, err := exec.LookPath(config.Model); err != nil { // It might be a built-in shell command, so don't fail hard // Just warn return nil } } return nil } // SetWorkingDirectory sets the working directory for terminal commands func (p *TerminalProvider) SetWorkingDirectory(dir string) { p.mu.Lock() defer p.mu.Unlock() p.workingDir = dir } // SetEnvironment sets additional environment variables func (p *TerminalProvider) SetEnvironment(env map[string]string) { p.mu.Lock() defer p.mu.Unlock() // Start with current environment p.env = os.Environ() // Add/override with provided variables for key, value := range env { p.env = append(p.env, fmt.Sprintf("%s=%s", key, value)) } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server