Back to Articles

LunaPipe: Bringing ChatGPT into the Unix Pipeline

[ View on GitHub ]

LunaPipe: Bringing ChatGPT into the Unix Pipeline

Hook

What if you could pipe your git diff through ChatGPT to generate commit messages, or transform error logs into documentation, all without leaving your terminal?

Context

The rise of large language models created an awkward friction point for terminal-dwelling developers. While ChatGPT excels at text transformation tasks—summarizing logs, generating code, explaining errors—accessing it meant context-switching to a web browser, copy-pasting text back and forth, and breaking the flow state. Early CLI wrappers treated LLMs as interactive chat applications, missing the fundamental insight that for developers, an LLM is just another text transformation tool that should compose with grep, awk, and every other Unix utility.

LunaPipe solves this by treating ChatGPT as a proper Unix citizen. It reads from stdin, writes to stdout, and can slot into any pipeline. The tool emerged from the lunabrain-ai team's need to integrate LLM capabilities into their existing shell scripts and automation workflows without rewriting everything as Python scripts or JavaScript applications. By implementing streaming responses and a template system for common patterns, LunaPipe bridges the gap between the modern LLM ecosystem and the 50-year-old Unix philosophy of small, composable tools.

Technical Insight

At its core, LunaPipe is a streaming HTTP client wrapped in Unix pipeline semantics. The architecture centers on three main components: a configuration manager that handles OpenAI API credentials stored in ~/.lunapipe/config.yaml, a streaming request handler that processes Server-Sent Events from OpenAI's chat completion endpoint, and a template engine built on Go's text/template package.

The streaming implementation is particularly elegant. Rather than waiting for the entire response before outputting anything, LunaPipe processes each token as it arrives and immediately writes it to stdout. This creates a typewriter effect in the terminal and, more importantly, allows downstream pipeline stages to start processing before the LLM finishes generating:

# Generate a script and pipe it directly to execution
echo "create a bash script that lists all git repos in current directory" | \
  lunapipe | \
  tee generated_script.sh | \
  bash

# Transform error logs into user-friendly messages in real-time
tail -f /var/log/app.log | \
  grep ERROR | \
  lunapipe "explain this error to a non-technical user" | \
  slack-notifier --channel #support

The template system provides reusable prompt patterns without forcing you into a heavyweight framework. Templates are stored in ~/.lunapipe/templates/ as simple text files with Go template syntax for parameter substitution:

// Example template: ~/.lunapipe/templates/docstring.txt
// Generate a docstring for the following {{.Language}} function.
// Follow {{.Style}} style conventions.
// Function:
// {{.Code}}

You invoke templates by passing parameters as key-value pairs:

cat myfunction.go | lunapipe -t docstring \
  Language=Go \
  Style=godoc \
  Code="$(cat myfunction.go)"

The Go implementation keeps dependencies minimal—essentially just the standard library plus OpenAI's API client. The main event loop uses Go's bufio.Scanner for reading stdin line-by-line and channels for handling the streaming response. This makes the codebase approachable for auditing and extension. The entire core logic fits in a few hundred lines:

// Simplified core streaming logic
func streamCompletion(prompt string, stdin io.Reader) error {
    client := openai.NewClient(apiKey)
    
    // Build messages array from stdin and prompt
    messages := buildMessages(stdin, prompt)
    
    stream, err := client.CreateChatCompletionStream(
        context.Background(),
        openai.ChatCompletionRequest{
            Model:    "gpt-4",
            Messages: messages,
            Stream:   true,
        },
    )
    
    // Stream tokens directly to stdout
    for {
        response, err := stream.Recv()
        if errors.Is(err, io.EOF) {
            return nil
        }
        if err != nil {
            return err
        }
        
        fmt.Print(response.Choices[0].Delta.Content)
    }
}

The three operational modes—one-off prompts, interactive chat, and templated execution—all share this same streaming core but differ in how they construct the message array. One-off mode combines stdin with command-line arguments, interactive mode maintains a conversation history in memory, and template mode substitutes parameters before constructing the prompt. This architectural decision keeps the codebase DRY while providing flexibility for different use cases.

One particularly clever aspect is how LunaPipe handles API errors. Instead of dying silently or dumping JSON errors, it formats API failures as human-readable messages to stderr while preserving the pipeline. This means a failing LunaPipe command in a complex pipeline won't corrupt downstream processing—you'll see the error but can handle it gracefully with standard Unix error handling patterns.

Gotcha

The most significant limitation is OpenAI lock-in. LunaPipe hardcodes OpenAI's API endpoints and authentication scheme, so you can't swap in Anthropic's Claude, local LLaMA models, or any other provider without forking the code. This becomes problematic if you want to use different models for different tasks—maybe GPT-4 for complex reasoning but a local model for sensitive data. The architecture doesn't include a provider abstraction layer, so adding multi-provider support would require substantial refactoring.

Conversation context handling in pipeline mode is practically nonexistent. Each invocation is stateless—there's no way to reference previous pipeline stages or maintain conversation history across commands. If you're piping three transformations through LunaPipe sequentially, each one hits the API independently with no awareness of the others. This wastes tokens and breaks workflows where context accumulation matters. The interactive chat mode does maintain history, but it's in-memory only and disappears when you exit. There's no session persistence, no way to save interesting conversations, and no mechanism to inject historical context into piped commands. For one-off transformations this is fine, but it severely limits more sophisticated use cases where you're building up context iteratively.

Verdict

Use if: You live in the terminal and want to integrate LLM capabilities into existing shell scripts, automation workflows, or one-off text transformations without adding heavyweight dependencies. LunaPipe excels when you need quick, scriptable access to ChatGPT and want to compose it with other Unix tools. It's ideal for teams already standardized on OpenAI who value simplicity and auditability over feature breadth. Skip if: You need multi-provider support, require persistent conversation history across invocations, or want to integrate local/self-hosted models. Also skip if you're building complex LLM workflows with conditional logic, conversation branching, or sophisticated state management—LunaPipe's simplicity becomes a limitation at that scale. Consider shell-gpt or aichat instead for those scenarios.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/lunabrain-ai-lunapipe.svg)](https://starlog.is/api/badge-click/developer-tools/lunabrain-ai-lunapipe)