This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-core/main.go

264 lines
6.2 KiB
Go

package main
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"ollie/config"
"ollie/mcp"
"ollie/ollama"
"ollie/tools"
)
type model struct {
textarea textarea.Model
viewport viewport.Model
messages []ollama.Message
client *ollama.Client
tools []tools.ToolInfo
executor *tools.Executor
model string
history []string
ready bool
hooks map[string]string
}
func main() {
if len(os.Args) < 2 {
log.Fatal("Usage: ollama-mcp-client <config.json> [model]")
}
modelName := "qwen3:8b"
if len(os.Args) > 2 {
modelName = os.Args[2]
}
cfg, err := config.Load(os.Args[1])
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
hooks := make(map[string]string)
if cfg.Hooks != nil {
hooks = cfg.Hooks
}
executor := tools.NewExecutor()
connectedServers := 0
for name, serverCfg := range cfg.MCPServers {
if serverCfg.Disabled {
log.Printf("Skipping disabled server: %s", name)
continue
}
if serverCfg.Command != "" {
transport := mcp.NewSTDIOTransport(serverCfg.Command, serverCfg.Args, serverCfg.Env)
client, err := transport.Connect()
if err != nil {
log.Printf("Failed to connect to %s: %v", name, err)
continue
}
executor.AddServer(name, client)
connectedServers++
log.Printf("Connected to server: %s", name)
}
}
if connectedServers == 0 {
log.Fatal("No MCP servers connected")
}
toolsList, err := executor.ListTools()
if err != nil {
log.Fatalf("Failed to list tools: %v", err)
}
log.Printf("Loaded %d tools", len(toolsList))
ta := textarea.New()
ta.Placeholder = "Type your message..."
ta.Prompt = ""
ta.ShowLineNumbers = false
ta.CharLimit = 0
ta.SetHeight(5)
ta.KeyMap.InsertNewline = key.NewBinding(key.WithKeys("ctrl+j"))
ta.Focus()
p := tea.NewProgram(model{
textarea: ta,
client: ollama.NewClient("http://localhost:11434"),
tools: toolsList,
executor: executor,
model: modelName,
hooks: hooks,
})
if hook := hooks["agentSpawn"]; hook != "" {
exec.Command("sh", "-c", hook).Run()
}
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
func (m model) Init() tea.Cmd {
return textarea.Blink
}
type responseMsg struct {
history []string
messages []ollama.Message
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
if !m.ready {
m.viewport = viewport.New(msg.Width, msg.Height-5)
m.ready = true
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - 5
}
m.textarea.SetWidth(msg.Width)
case responseMsg:
m.history = msg.history
m.messages = msg.messages
m.viewport.SetContent(strings.Join(m.history, "\n"))
m.viewport.GotoBottom()
return m, nil
case tea.KeyMsg:
if msg.String() == "ctrl+c" || msg.String() == "esc" {
return m, tea.Quit
}
if msg.String() == "enter" {
input := strings.TrimSpace(m.textarea.Value())
if input == "" {
return m, nil
}
m.history = append(m.history, "You: "+input)
m.viewport.SetContent(strings.Join(m.history, "\n"))
m.viewport.GotoBottom()
m.textarea.Reset()
if hook := m.hooks["userPromptSubmit"]; hook != "" {
exec.Command("sh", "-c", hook).Run()
}
return m, m.handleInputCmd(input)
}
}
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
func (m model) handleInputCmd(input string) tea.Cmd {
return func() tea.Msg {
history := append([]string{}, m.history...)
messages := append([]ollama.Message{}, m.messages...)
messages = append(messages, ollama.Message{Role: "user", Content: input})
ollamaTools := make([]ollama.Tool, len(m.tools))
for i, t := range m.tools {
ollamaTools[i] = ollama.Tool{
Type: "function",
Function: ollama.Function{
Name: t.Name,
Description: t.Description,
Parameters: t.InputSchema,
},
}
}
resp, err := m.client.Chat(ollama.ChatRequest{
Model: m.model,
Messages: messages,
Tools: ollamaTools,
Stream: false,
})
if err != nil {
history = append(history, fmt.Sprintf("Error: %v", err))
return responseMsg{history: history, messages: messages}
}
messages = append(messages, resp.Message)
seenCalls := make(map[string]bool)
for len(resp.Message.ToolCalls) > 0 {
for _, tc := range resp.Message.ToolCalls {
callKey := fmt.Sprintf("%s:%v", tc.Function.Name, tc.Function.Arguments)
if seenCalls[callKey] {
history = append(history, fmt.Sprintf("Skipping duplicate tool call: %s", tc.Function.Name))
continue
}
seenCalls[callKey] = true
history = append(history, fmt.Sprintf("Running tool: %s", tc.Function.Name))
var toolInfo *tools.ToolInfo
for _, t := range m.tools {
if t.Name == tc.Function.Name {
toolInfo = &t
break
}
}
if toolInfo == nil {
history = append(history, fmt.Sprintf("→ %s: error - tool not found", tc.Function.Name))
continue
}
result, err := m.executor.Execute(toolInfo.Server, tc.Function.Name, tc.Function.Arguments)
if err != nil {
history = append(history, fmt.Sprintf("→ %s: error - %v", tc.Function.Name, err))
continue
}
resultStr := string(result)
history = append(history, fmt.Sprintf("→ %s: success - %s", tc.Function.Name, resultStr))
messages = append(messages, ollama.Message{
Role: "tool",
Content: resultStr,
})
}
resp, err = m.client.Chat(ollama.ChatRequest{
Model: m.model,
Messages: messages,
Tools: ollamaTools,
Stream: false,
})
if err != nil {
history = append(history, fmt.Sprintf("Error: %v", err))
return responseMsg{history: history, messages: messages}
}
messages = append(messages, resp.Message)
}
if resp.Message.Content != "" {
history = append(history, "Bot: "+resp.Message.Content)
}
if hook := m.hooks["stop"]; hook != "" {
exec.Command("sh", "-c", hook).Run()
}
return responseMsg{history: history, messages: messages}
}
}
func (m model) View() string {
if !m.ready {
return "Loading..."
}
return m.viewport.View() + "\n" + m.textarea.View()
}