Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Lumo is an intelligent CLI assistant that interprets natural language to help yo
- **Pipe Support**: Analyze and explain command outputs
- **Web Interface**: Access Lumo through a browser-based interface
- **Secure Authentication**: JWT-based authentication for the REST API
- **Multiple AI Providers**: Support for Google Gemini, OpenAI, and Ollama
- **Multiple AI Providers**: Google Gemini, OpenAI, Anthropic Claude, Ollama, and any OpenAI-compatible API (xAI Grok, DeepSeek, Mistral, Groq, OpenRouter)

## 🚀 Installation

Expand All @@ -45,12 +45,12 @@ Lumo is an intelligent CLI assistant that interprets natural language to help yo
```bash

# Using pre-built binary (Linux)
curl -L https://github.com/agnath18K/lumo/releases/download/v1.0.2/lumo_1.0.2_linux_amd64.tar.gz -o lumo.tar.gz
curl -L https://github.com/agnath18K/lumo/releases/download/v1.1.0/lumo_1.1.0_linux_amd64.tar.gz -o lumo.tar.gz
tar -xzf lumo.tar.gz
sudo mv lumo /usr/local/bin/

# Using Debian package
curl -L https://github.com/agnath18K/lumo/releases/download/v1.0.2/lumo_1.0.2_amd64.deb -o lumo.deb
curl -L https://github.com/agnath18K/lumo/releases/download/v1.1.0/lumo_1.1.0_amd64.deb -o lumo.deb
sudo dpkg -i lumo.deb
```

Expand Down Expand Up @@ -90,6 +90,31 @@ lumo server:start
lumo config:server auth enable
```

### Configuring AI providers

```bash
# List providers and the current selection
lumo config:provider list

# Anthropic Claude (set ANTHROPIC_API_KEY or use config:key)
lumo config:key set claude <key>
lumo config:provider set claude
lumo config:model set claude-sonnet-4-6 # or claude-opus-4-8 / claude-haiku-4-5 / claude-fable-5

# Any OpenAI-compatible API (xAI, DeepSeek, Mistral, Groq, OpenRouter)
lumo config:compatible set-url https://api.x.ai/v1
lumo config:key set openai-compatible <key>
lumo config:provider set openai-compatible
lumo config:model set grok-4 # any model the endpoint serves

# Model IDs are not gated — any string is accepted, so new models work
# without a CLI update. `config:model list` shows current suggestions.
```

Provider API keys can also be supplied via environment variables:
`GEMINI_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and
`OPENAI_COMPATIBLE_API_KEY` / `OPENAI_COMPATIBLE_BASE_URL`.

**For complete usage documentation and examples, visit [getlumo.dev/documentation](https://getlumo.dev/documentation)**

**For information about the authentication system, see [Authentication Documentation](docs/authentication.md)**
Expand Down
10 changes: 10 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
lumo (1.1.0-1) unstable; urgency=medium

* Version 1.1.0 release
* Add Anthropic Claude provider (native Messages API)
* Add generic OpenAI-compatible provider (xAI, DeepSeek, Mistral, Groq, OpenRouter)
* Permissive model selection: accept any model string with curated suggestions
* Modernize default models (OpenAI default now gpt-4o-mini)

-- agnath18K <agnath18@gmail.com> Tue, 23 Jun 2026 00:00:00 +0530

lumo (1.0.2-1) unstable; urgency=medium

* Version 1.0.2 release
Expand Down
13 changes: 11 additions & 2 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,22 +375,27 @@ lumo config:provider show
lumo config:provider set gemini
lumo config:provider set openai
lumo config:provider set ollama
lumo config:provider set claude
lumo config:provider set openai-compatible

# List available models for the current provider
lumo config:model list

# Show current model
lumo config:model show

# Set model for current provider
lumo config:model set gemini-2.0-flash-lite
# Set model for current provider (any model string is accepted)
lumo config:model set gemini-2.5-flash-lite
lumo config:model set claude-sonnet-4-6

# Show API key status
lumo config:key show

# Set API key for a provider
lumo config:key set gemini YOUR_API_KEY
lumo config:key set openai YOUR_API_KEY
lumo config:key set claude YOUR_API_KEY
lumo config:key set openai-compatible YOUR_API_KEY

# Remove API key for a provider
lumo config:key remove gemini
Expand All @@ -403,6 +408,10 @@ lumo config:ollama set http://localhost:11434

# Test connection to Ollama server
lumo config:ollama test

# Configure an OpenAI-compatible endpoint (xAI, DeepSeek, Mistral, Groq, OpenRouter)
lumo config:compatible show
lumo config:compatible set-url https://api.x.ai/v1
```

## Pipe Support
Expand Down
27 changes: 15 additions & 12 deletions docs/lumo.1
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ List available AI providers.
Show current AI provider.
.TP
.B lumo config:provider set \fIPROVIDER\fR
Set AI provider (gemini, openai, ollama).
Set AI provider (gemini, openai, ollama, claude, openai-compatible).
.TP
.B lumo config:model list
List available models for the current provider.
Expand Down Expand Up @@ -182,6 +182,12 @@ Set Ollama URL.
.TP
.B lumo config:ollama test
Test connection to Ollama server.
.TP
.B lumo config:compatible show
Show the OpenAI-compatible endpoint base URL and model.
.TP
.B lumo config:compatible set-url \fIURL\fR
Set the OpenAI-compatible endpoint base URL (e.g. https://api.x.ai/v1).

.SS File Transfer with Connect
Transfer files between machines:
Expand Down Expand Up @@ -469,23 +475,20 @@ Configuration file that stores user preferences, API keys, and other settings.

.SH ENVIRONMENT
.TP
.B LUMO_AI_PROVIDER
Sets the AI provider to use (gemini, openai, ollama).
.TP
.B LUMO_GEMINI_API_KEY
.B GEMINI_API_KEY
Sets the API key for Google Gemini.
.TP
.B LUMO_OPENAI_API_KEY
.B OPENAI_API_KEY
Sets the API key for OpenAI.
.TP
.B LUMO_OLLAMA_URL
Sets the URL for the Ollama server.
.B ANTHROPIC_API_KEY
Sets the API key for Anthropic Claude.
.TP
.B LUMO_ENABLE_AGENT_MODE
Enables or disables agent mode.
.B OPENAI_COMPATIBLE_API_KEY
Sets the API key for the OpenAI-compatible provider (xAI, DeepSeek, Mistral, Groq, OpenRouter).
.TP
.B LUMO_DEBUG
Enables or disables debug mode.
.B OPENAI_COMPATIBLE_BASE_URL
Sets the base URL for the OpenAI-compatible provider (e.g. https://api.x.ai/v1).

.SH SEE ALSO
.BR curl (1),
Expand Down
207 changes: 207 additions & 0 deletions pkg/ai/claude.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package ai

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)

// anthropicVersion is the required API version header value for the Messages API.
const anthropicVersion = "2023-06-01"

// claudeDefaultMaxTokens is the default output token budget for a request.
// max_tokens is required by the Anthropic Messages API.
const claudeDefaultMaxTokens = 4096

// ClaudeClient implements the Client interface for Anthropic's Messages API
type ClaudeClient struct {
apiKey string
model string
client *http.Client
}

// ClaudeMessage represents a single message in a Claude request.
// Roles are limited to "user" and "assistant"; the system prompt is a
// top-level field, not a message.
type ClaudeMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}

// ClaudeRequest represents a request to the Anthropic Messages API
type ClaudeRequest struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"`
Messages []ClaudeMessage `json:"messages"`
}

// ClaudeContentBlock represents a single content block in a Claude response
type ClaudeContentBlock struct {
Type string `json:"type"`
Text string `json:"text"`
}

// ClaudeError represents an error returned by the Anthropic API
type ClaudeError struct {
Type string `json:"type"`
Message string `json:"message"`
}

// ClaudeResponse represents a response from the Anthropic Messages API
type ClaudeResponse struct {
Content []ClaudeContentBlock `json:"content"`
Error *ClaudeError `json:"error,omitempty"`
}

// NewClaudeClient creates a new Anthropic Claude client
func NewClaudeClient(apiKey string, model string) *ClaudeClient {
// If model is empty, use a sensible default (best speed/cost balance)
if model == "" {
model = "claude-sonnet-4-6"
}

return &ClaudeClient{
apiKey: apiKey,
model: model,
client: &http.Client{},
}
}

// send issues a Messages API request and returns the first text block.
func (c *ClaudeClient) send(ctx context.Context, system string, messages []ClaudeMessage) (string, error) {
reqBody := ClaudeRequest{
Model: c.model,
MaxTokens: claudeDefaultMaxTokens,
System: system,
Messages: messages,
}

jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("error marshaling request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST", "https://api.anthropic.com/v1/messages", bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", c.apiKey)
req.Header.Set("anthropic-version", anthropicVersion)

resp, err := c.client.Do(req)
if err != nil {
return "", fmt.Errorf("error sending request: %w", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response: %w", err)
}

var claudeResp ClaudeResponse
if err := json.Unmarshal(body, &claudeResp); err != nil {
return "", fmt.Errorf("error parsing response: %w", err)
}

if claudeResp.Error != nil {
return "", fmt.Errorf("API error: %s", claudeResp.Error.Message)
}

// Return the first text block from the response
for _, block := range claudeResp.Content {
if block.Type == "text" {
return block.Text, nil
}
}

return "", fmt.Errorf("empty response from API")
}

// systemPrompt builds the shared Lumo system prompt including environment context.
func systemPrompt() string {
pwd, err := os.Getwd()
if err != nil {
pwd = "unknown"
}
return fmt.Sprintf("You are Lumo, an AI assistant in the terminal. Be concise and helpful.\n\n%s\n\n%s\n\nCurrent Working Directory: %s",
SystemInstructions, EnvContext(), pwd)
}

// Query sends a query to the Anthropic API and returns the response
func (c *ClaudeClient) Query(query string) (string, error) {
return c.send(context.Background(), systemPrompt(), []ClaudeMessage{
{Role: "user", Content: query},
})
}

// GetCompletion sends a prompt to the Anthropic API and returns the completion
func (c *ClaudeClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
return c.send(ctx, "", []ClaudeMessage{
{Role: "user", Content: prompt},
})
}

// ProcessChatMessage processes a chat message with conversation history
// and returns the AI response. The conversation is parsed the same way as the
// OpenAI client; "system:" lines are merged into the top-level system prompt
// since Claude does not accept a system role inside the messages array.
func (c *ClaudeClient) ProcessChatMessage(ctx context.Context, conversation string) (string, error) {
var system strings.Builder
var messages []ClaudeMessage

lines := strings.Split(conversation, "\n")
var currentRole string
var currentContent strings.Builder

flush := func() {
if currentRole == "" || currentContent.Len() == 0 {
return
}
if currentRole == "system" {
if system.Len() > 0 {
system.WriteString("\n\n")
}
system.WriteString(currentContent.String())
} else {
messages = append(messages, ClaudeMessage{Role: currentRole, Content: currentContent.String()})
}
currentContent.Reset()
}

for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}

switch {
case strings.HasPrefix(line, "system:"):
flush()
currentRole = "system"
currentContent.WriteString(strings.TrimSpace(line[7:]))
case strings.HasPrefix(line, "user:"):
flush()
currentRole = "user"
currentContent.WriteString(strings.TrimSpace(line[5:]))
case strings.HasPrefix(line, "assistant:"):
flush()
currentRole = "assistant"
currentContent.WriteString(strings.TrimSpace(line[10:]))
default:
if currentRole != "" {
currentContent.WriteString(" " + line)
}
}
}
flush()

return c.send(ctx, system.String(), messages)
}
Loading
Loading