Files
gnoma/cmd/gnoma/main.go
vikingowl f0633d8ac6 feat: complete M1 — core engine with Mistral provider
Mistral provider adapter with streaming, tool calls (single-chunk
pattern), stop reason inference, model listing, capabilities, and
JSON output support.

Tool system: bash (7 security checks, shell alias harvesting for
bash/zsh/fish), file ops (read, write, edit, glob, grep, ls).
Alias harvesting collects 300+ aliases from user's shell config.

Engine agentic loop: stream → tool execution → re-query → until
done. Tool gating on model capabilities. Max turns safety limit.

CLI pipe mode: echo "prompt" | gnoma streams response to stdout.
Flags: --provider, --model, --system, --api-key, --max-turns,
--verbose, --version.

Provider interface expanded: Models(), DefaultModel(), Capabilities
(ToolUse, JSONOutput, Vision, Thinking, ContextWindow, MaxOutput),
ResponseFormat with JSON schema support.

Live verified: text streaming + tool calling with devstral-small.
117 tests across 8 packages, 10MB binary.
2026-04-03 12:01:55 +02:00

198 lines
4.8 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"strings"
"somegit.dev/Owlibou/gnoma/internal/engine"
"somegit.dev/Owlibou/gnoma/internal/provider"
"somegit.dev/Owlibou/gnoma/internal/provider/mistral"
"somegit.dev/Owlibou/gnoma/internal/stream"
"somegit.dev/Owlibou/gnoma/internal/tool"
"somegit.dev/Owlibou/gnoma/internal/tool/bash"
"somegit.dev/Owlibou/gnoma/internal/tool/fs"
)
func main() {
var (
providerName = flag.String("provider", "mistral", "LLM provider")
model = flag.String("model", "", "model name (empty = provider default)")
system = flag.String("system", defaultSystem, "system prompt")
apiKey = flag.String("api-key", "", "API key (or set MISTRAL_API_KEY env)")
maxTurns = flag.Int("max-turns", 50, "max tool-calling rounds per turn")
verbose = flag.Bool("verbose", false, "enable debug logging")
version = flag.Bool("version", false, "print version and exit")
)
flag.Parse()
if *version {
fmt.Println("gnoma v0.1.0-dev")
os.Exit(0)
}
// Logger
logLevel := slog.LevelWarn
if *verbose {
logLevel = slog.LevelDebug
}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))
// Resolve API key
key := *apiKey
if key == "" {
key = resolveAPIKey(*providerName)
}
if key == "" {
fmt.Fprintf(os.Stderr, "error: no API key for provider %q\nSet %s environment variable or use --api-key\n",
*providerName, envKeyFor(*providerName))
os.Exit(1)
}
// Create provider
prov, err := createProvider(*providerName, key, *model)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
// Create tool registry
reg := buildToolRegistry()
// Harvest shell aliases
aliases, err := bash.HarvestAliases(context.Background())
if err != nil {
logger.Debug("alias harvest failed (non-fatal)", "error", err)
} else {
logger.Debug("harvested aliases", "count", aliases.Len())
}
// Re-register bash tool with aliases
reg.Register(bash.New(bash.WithAliases(aliases)))
// Create engine
eng, err := engine.New(engine.Config{
Provider: prov,
Tools: reg,
System: *system,
Model: *model,
MaxTurns: *maxTurns,
Logger: logger,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
// Read input
input, err := readInput(flag.Args())
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if input == "" {
fmt.Fprintln(os.Stderr, "error: no input provided")
fmt.Fprintln(os.Stderr, "usage: echo 'prompt' | gnoma")
fmt.Fprintln(os.Stderr, " or: gnoma 'prompt'")
os.Exit(1)
}
// Context with signal handling
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
// Callback: stream text deltas to stdout
cb := func(evt stream.Event) {
if evt.Type == stream.EventTextDelta && evt.Text != "" {
fmt.Print(evt.Text)
}
}
// Submit and run
_, err = eng.Submit(ctx, input, cb)
fmt.Println() // final newline
if err != nil {
if ctx.Err() != nil {
fmt.Fprintln(os.Stderr, "\ninterrupted")
os.Exit(130)
}
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func readInput(args []string) (string, error) {
// Positional args
if len(args) > 0 {
return strings.Join(args, " "), nil
}
// Stdin (pipe mode)
stat, _ := os.Stdin.Stat()
if stat.Mode()&os.ModeCharDevice == 0 {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("reading stdin: %w", err)
}
return strings.TrimSpace(string(data)), nil
}
return "", nil
}
func resolveAPIKey(providerName string) string {
envVar := envKeyFor(providerName)
return os.Getenv(envVar)
}
func envKeyFor(providerName string) string {
switch providerName {
case "mistral":
return "MISTRAL_API_KEY"
case "anthropic":
return "ANTHROPIC_API_KEY"
case "openai":
return "OPENAI_API_KEY"
case "google":
return "GEMINI_API_KEY"
default:
return strings.ToUpper(providerName) + "_API_KEY"
}
}
func createProvider(name, apiKey, model string) (provider.Provider, error) {
cfg := provider.ProviderConfig{
APIKey: apiKey,
Model: model,
}
switch name {
case "mistral":
return mistral.New(cfg)
default:
return nil, fmt.Errorf("unknown provider %q (M1 supports: mistral)", name)
}
}
func buildToolRegistry() *tool.Registry {
reg := tool.NewRegistry()
reg.Register(bash.New())
reg.Register(fs.NewReadTool())
reg.Register(fs.NewWriteTool())
reg.Register(fs.NewEditTool())
reg.Register(fs.NewGlobTool())
reg.Register(fs.NewGrepTool())
reg.Register(fs.NewLSTool())
return reg
}
const defaultSystem = `You are gnoma, a provider-agnostic agentic coding assistant.
You help users with software engineering tasks by reading files, writing code, and executing commands.
Be concise and direct. Use tools when needed to accomplish the task.`