stromboli

package module
v0.3.1-alpha Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 5, 2026 License: MIT Imports: 27 Imported by: 0

README

Stromboli Go SDK 🌋

Go Reference Go Report Card

Official Go SDK for Stromboli — Container orchestration for Claude Code agents.

Stromboli provides a secure, isolated environment for running Claude Code in Podman containers. This SDK offers a clean, idiomatic Go interface to interact with the Stromboli API.

Table of Contents

Features

  • Full API Coverage — All Stromboli endpoints supported
  • Type Safety — Strongly typed requests and responses
  • Streaming — Real-time SSE streaming for Claude output
  • Context Support — Cancellation and timeouts via context
  • Retries — Automatic retries with exponential backoff
  • Idiomatic Go — Follows Go best practices and conventions

Installation

go get github.com/tomblancdev/stromboli-go

Requirements:

  • Go 1.22 or later
  • A running Stromboli instance

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/tomblancdev/stromboli-go"
)

func main() {
    // Create a client
    client, err := stromboli.NewClient("http://localhost:8585")
    if err != nil {
        log.Fatal(err)
    }

    // Execute Claude synchronously
    result, err := client.Run(context.Background(), &stromboli.RunRequest{
        Prompt: "Hello, Claude! Write a haiku about Go programming.",
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(result.Output)
}

Core Concepts

Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│                        Your Application                          │
└─────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Stromboli Go SDK                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │   Client    │  │   Types     │  │  Streaming  │              │
│  └─────────────┘  └─────────────┘  └─────────────┘              │
└─────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼ HTTP/SSE
┌─────────────────────────────────────────────────────────────────┐
│                      Stromboli API Server                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │    Jobs     │  │  Sessions   │  │   Secrets   │              │
│  └─────────────┘  └─────────────┘  └─────────────┘              │
└─────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Podman Containers                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                   Claude Code Agent                      │    │
│  │  • Isolated execution environment                        │    │
│  │  • Resource limits (CPU, memory)                         │    │
│  │  • Volume mounts for workspace access                    │    │
│  │  • Secret injection                                      │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
Execution Modes
Mode Method Use Case
Synchronous Run() Short tasks, immediate response needed
Asynchronous RunAsync() Long tasks, polling or webhooks
Streaming Stream() Real-time output, interactive UIs
Sessions

Sessions enable conversation continuity — Claude remembers context across multiple interactions:

// First interaction
result1, _ := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "My name is Alice and I'm working on a Go project.",
})

// Continue the conversation (Claude remembers context)
result2, _ := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "What's my name and what am I working on?",
    Claude: &stromboli.ClaudeOptions{
        SessionID: result1.SessionID,
        Resume:    true,
    },
})

API Reference

Client Configuration

Create a client with default settings:

client, err := stromboli.NewClient("http://localhost:8585")
if err != nil {
    log.Fatal(err)
}

Configure with options:

client, err := stromboli.NewClient("http://localhost:8585",
    stromboli.WithTimeout(5*time.Minute),     // Request timeout
    stromboli.WithRetries(3),                  // Retry on transient errors
    stromboli.WithToken("your-jwt-token"),     // Pre-set auth token
    stromboli.WithUserAgent("my-app/1.0.0"),   // Custom User-Agent
    stromboli.WithHTTPClient(customClient),    // Custom HTTP client
)
if err != nil {
    log.Fatal(err)
}
Options Reference
Option Description Default
WithTimeout(d) Request timeout 30s
WithRetries(n) Max retry attempts 0
WithToken(t) Bearer token for auth ""
WithUserAgent(ua) User-Agent header "stromboli-go/{version}"
WithHTTPClient(c) Custom HTTP client http.DefaultClient

Execution
Run (Synchronous)

Execute Claude and wait for the complete response:

result, err := client.Run(ctx, &stromboli.RunRequest{
    Prompt:  "Analyze this code for bugs",
    Workdir: "/workspace",
    Claude: &stromboli.ClaudeOptions{
        Model:        stromboli.ModelSonnet,  // Model selection
        MaxBudgetUSD: 5.0,                    // Cost limit
        AllowedTools: []string{"Read", "Grep", "Glob"},
    },
    Podman: &stromboli.PodmanOptions{
        Memory:  "2g",                        // Memory limit
        Timeout: "10m",                       // Execution timeout
        Volumes: []string{"/code:/workspace:ro"},
    },
})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Status: %s\n", result.Status)
fmt.Printf("Output: %s\n", result.Output)
fmt.Printf("Session: %s\n", result.SessionID)
RunRequest Fields
Field Type Description
Prompt string Required. The prompt to send to Claude
Workdir string Working directory inside container
WebhookURL string URL for completion notification
Claude *ClaudeOptions Claude-specific configuration
Podman *PodmanOptions Container configuration
ClaudeOptions
Field Type Description
Model string Model: ModelSonnet, ModelOpus, ModelHaiku
SessionID string Session ID for conversation continuity
Resume bool Resume existing session
MaxBudgetUSD float64 Maximum spend in USD
SystemPrompt string Override system prompt
AppendSystemPrompt string Append to system prompt
AllowedTools []string Whitelist of allowed tools
DisallowedTools []string Blacklist of tools
PermissionMode string Permission mode
OutputFormat string Output format
Verbose bool Verbose output
Debug bool Debug mode
PodmanOptions
Field Type Description
Memory string Memory limit (e.g., "512m", "2g")
Timeout string Execution timeout (e.g., "5m", "1h")
Cpus string CPU limit
CPUShares int64 CPU shares
Volumes []string Volume mounts
Image string Custom container image
SecretsEnv map[string]string Secrets to inject as env vars
RunAsync (Asynchronous)

Start a long-running task and get a job ID:

job, err := client.RunAsync(ctx, &stromboli.RunRequest{
    Prompt:     "Review the entire codebase for security issues",
    WebhookURL: "https://example.com/webhook", // Optional notification
})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Job started: %s\n", job.JobID)

// Poll for completion
for {
    status, _ := client.GetJob(ctx, job.JobID)

    switch {
    case status.IsCompleted():
        fmt.Println(status.Output)
        return
    case status.IsFailed():
        log.Fatalf("Job failed: %s", status.Error)
    default:
        fmt.Printf("Status: %s\n", status.Status)
        time.Sleep(2 * time.Second)
    }
}

Streaming

Stream Claude's output in real-time using Server-Sent Events (SSE):

stream, err := client.Stream(ctx, &stromboli.StreamRequest{
    Prompt:    "Count from 1 to 10 slowly",
    SessionID: "optional-session-id",
})
if err != nil {
    log.Fatal(err)
}
defer stream.Close()

// Iterator pattern
for stream.Next() {
    event := stream.Event()
    fmt.Print(event.Data) // Print as it arrives
}

if err := stream.Err(); err != nil {
    log.Fatal(err)
}
Channel-based Iteration
stream, _ := client.Stream(ctx, req)
defer stream.Close()

for event := range stream.Events() {
    switch event.Type {
    case "":
        fmt.Print(event.Data) // Regular output
    case "error":
        log.Printf("Error: %s", event.Data)
    case "done":
        fmt.Println("\nStream complete")
    }
}
StreamEvent Fields
Field Type Description
Type string Event type ("", "error", "done")
Data string Event payload
ID string Event ID (if provided)

Jobs
List All Jobs
jobs, err := client.ListJobs(ctx)
if err != nil {
    log.Fatal(err)
}

for _, job := range jobs {
    fmt.Printf("%s: %s (created: %s)\n",
        job.ID, job.Status, job.CreatedAt)
}
Get Job Status
job, err := client.GetJob(ctx, "job-abc123def456")
if err != nil {
    if errors.Is(err, stromboli.ErrNotFound) {
        fmt.Println("Job not found")
        return
    }
    log.Fatal(err)
}

fmt.Printf("ID: %s\n", job.ID)
fmt.Printf("Status: %s\n", job.Status)
fmt.Printf("Output: %s\n", job.Output)

// Helper methods
if job.IsRunning() { fmt.Println("Still running...") }
if job.IsCompleted() { fmt.Println("Done!") }
if job.IsFailed() { fmt.Println("Failed:", job.Error) }
Cancel a Job
err := client.CancelJob(ctx, "job-abc123def456")
if err != nil {
    log.Fatal(err)
}
fmt.Println("Job cancelled")
Job Status Values
Status Description
pending Job is queued
running Job is executing
completed Job finished successfully
failed Job failed with error
cancelled Job was cancelled

Sessions
List Sessions
sessions, err := client.ListSessions(ctx)
if err != nil {
    log.Fatal(err)
}

for _, id := range sessions {
    fmt.Printf("Session: %s\n", id)
}
Get Session Messages

Retrieve conversation history:

messages, err := client.GetMessages(ctx, "sess-abc123", &stromboli.GetMessagesOptions{
    Limit:  50,
    Offset: 0,
})
if err != nil {
    log.Fatal(err)
}

for _, msg := range messages.Messages {
    fmt.Printf("[%s] %s\n", msg.Type, msg.UUID)
}

// Pagination
if messages.HasMore {
    nextPage, _ := client.GetMessages(ctx, "sess-abc123", &stromboli.GetMessagesOptions{
        Limit:  50,
        Offset: messages.Offset + messages.Limit,
    })
    // Process next page...
}
Get Single Message
msg, err := client.GetMessage(ctx, "sess-abc123", "msg-uuid-456")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Type: %s\n", msg.Type)
fmt.Printf("Timestamp: %s\n", msg.Timestamp)
Destroy Session
err := client.DestroySession(ctx, "sess-abc123")
if err != nil {
    log.Fatal(err)
}
fmt.Println("Session destroyed")

Authentication

Stromboli supports JWT-based authentication:

Get Token
tokens, err := client.GetToken(ctx, "my-client-id")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Access Token: %s\n", tokens.AccessToken)
fmt.Printf("Expires In: %d seconds\n", tokens.ExpiresIn)

// Set token for future requests
client.SetToken(tokens.AccessToken)
Refresh Token
newTokens, err := client.RefreshToken(ctx, tokens.RefreshToken)
if err != nil {
    // Refresh token expired, need to re-authenticate
    log.Fatal(err)
}

client.SetToken(newTokens.AccessToken)
Validate Token
validation, err := client.ValidateToken(ctx)
if err != nil {
    log.Fatal(err)
}

if validation.Valid {
    fmt.Printf("Token valid for: %s\n", validation.Subject)
    fmt.Printf("Expires at: %d\n", validation.ExpiresAt)
}
Logout
result, err := client.Logout(ctx)
if err != nil {
    log.Fatal(err)
}

if result.Success {
    fmt.Println("Logged out successfully")
    client.SetToken("") // Clear the token
}

System
Health Check
health, err := client.Health(ctx)
if err != nil {
    log.Fatalf("API unreachable: %v", err)
}

fmt.Printf("API: %s v%s\n", health.Name, health.Version)
fmt.Printf("Status: %s\n", health.Status)

// Check components
for _, comp := range health.Components {
    status := "✅"
    if !comp.IsHealthy() {
        status = "❌"
    }
    fmt.Printf("%s %s: %s\n", status, comp.Name, comp.Status)
}
Claude Status
status, err := client.ClaudeStatus(ctx)
if err != nil {
    log.Fatal(err)
}

if status.Configured {
    fmt.Println("Claude is ready for execution")
} else {
    fmt.Printf("Claude not configured: %s\n", status.Message)
}
List Secrets
secrets, err := client.ListSecrets(ctx)
if err != nil {
    log.Fatal(err)
}

for _, name := range secrets {
    fmt.Printf("Secret: %s\n", name)
}

Version Compatibility

The SDK includes runtime version checking to ensure compatibility with the Stromboli API server.

Quick Check
health, _ := client.Health(ctx)
if !stromboli.IsCompatible(health.Version) {
    log.Printf("Warning: Server %s may not be compatible with SDK", health.Version)
}
Detailed Check
health, _ := client.Health(ctx)
result := stromboli.CheckCompatibility(health.Version)

switch result.Status {
case stromboli.Compatible:
    fmt.Printf("✅ Server %s is compatible\n", result.ServerVersion)
case stromboli.Incompatible:
    fmt.Printf("⚠️  %s\n", result.Message)
case stromboli.Unknown:
    fmt.Printf("❓ Could not determine: %s\n", result.Message)
}
Fail Fast
func main() {
    client, err := stromboli.NewClient(url)
    if err != nil {
        log.Fatal(err)
    }
    health, err := client.Health(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // Panics if incompatible
    stromboli.MustBeCompatible(health.Version)

    // Continue with compatible server...
}
Version Constants
Constant Description
stromboli.Version SDK version (e.g., "0.1.0")
stromboli.APIVersion Target API version (e.g., "0.3.0-alpha")
stromboli.APIVersionRange Supported range (e.g., ">=0.3.0-alpha <0.4.0")

Error Handling

The SDK uses typed errors for common failure cases:

result, err := client.Run(ctx, req)
if err != nil {
    var apiErr *stromboli.Error
    if errors.As(err, &apiErr) {
        switch apiErr.Code {
        case "NOT_FOUND":
            fmt.Println("Resource not found")
        case "UNAUTHORIZED":
            fmt.Println("Need to authenticate")
        case "TIMEOUT":
            fmt.Println("Request timed out")
        case "BAD_REQUEST":
            fmt.Println("Invalid request:", apiErr.Message)
        default:
            fmt.Printf("API error [%s]: %s\n", apiErr.Code, apiErr.Message)
        }
    }
    return
}
Error Types
Code HTTP Status Description
BAD_REQUEST 400 Invalid request parameters
UNAUTHORIZED 401 Authentication required
FORBIDDEN 403 Access denied
NOT_FOUND 404 Resource not found
TIMEOUT 408 Request timed out
RATE_LIMITED 429 Too many requests
INTERNAL 5xx Server error
CANCELLED - Request was cancelled
Sentinel Errors
if errors.Is(err, stromboli.ErrNotFound) {
    // Handle not found
}
if errors.Is(err, stromboli.ErrTimeout) {
    // Handle timeout
}
if errors.Is(err, stromboli.ErrUnauthorized) {
    // Handle auth error
}

Examples

Complete Chat Application
package main

import (
    "bufio"
    "context"
    "fmt"
    "os"

    "github.com/tomblancdev/stromboli-go"
)

func main() {
    client, err := stromboli.NewClient("http://localhost:8585")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
        os.Exit(1)
    }
    ctx := context.Background()

    var sessionID string
    scanner := bufio.NewScanner(os.Stdin)

    fmt.Println("Chat with Claude (type 'quit' to exit)")

    for {
        fmt.Print("\nYou: ")
        if !scanner.Scan() {
            break
        }
        input := scanner.Text()
        if input == "quit" {
            break
        }

        req := &stromboli.RunRequest{Prompt: input}
        if sessionID != "" {
            req.Claude = &stromboli.ClaudeOptions{
                SessionID: sessionID,
                Resume:    true,
            }
        }

        result, err := client.Run(ctx, req)
        if err != nil {
            fmt.Printf("Error: %v\n", err)
            continue
        }

        sessionID = result.SessionID
        fmt.Printf("\nClaude: %s\n", result.Output)
    }

    // Cleanup
    if sessionID != "" {
        client.DestroySession(ctx, sessionID)
    }
}
Streaming with Progress
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/tomblancdev/stromboli-go"
)

func main() {
    client, err := stromboli.NewClient("http://localhost:8585")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
        os.Exit(1)
    }

    stream, err := client.Stream(context.Background(), &stromboli.StreamRequest{
        Prompt: "Write a short story about a robot learning to paint",
    })
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to start stream: %v\n", err)
        os.Exit(1)
    }
    defer stream.Close()

    fmt.Println("Claude is writing...\n")

    for stream.Next() {
        event := stream.Event()
        fmt.Print(event.Data)
    }

    if err := stream.Err(); err != nil {
        fmt.Fprintf(os.Stderr, "\nStream error: %v\n", err)
        os.Exit(1)
    }

    fmt.Println("\n\nDone!")
}
Async Job with Webhook
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/tomblancdev/stromboli-go"
)

func main() {
    client, err := stromboli.NewClient("http://localhost:8585")
    if err != nil {
        panic(err)
    }
    ctx := context.Background()

    // Start async job
    job, err := client.RunAsync(ctx, &stromboli.RunRequest{
        Prompt:  "Perform a comprehensive security audit",
        Workdir: "/workspace",
        Podman: &stromboli.PodmanOptions{
            Timeout: "30m",
            Memory:  "4g",
            Volumes: []string{"/code:/workspace:ro"},
        },
    })
    if err != nil {
        panic(err)
    }

    fmt.Printf("Job started: %s\n", job.JobID)

    // Poll with exponential backoff
    backoff := time.Second
    for {
        status, err := client.GetJob(ctx, job.JobID)
        if err != nil {
            panic(err)
        }

        switch {
        case status.IsCompleted():
            fmt.Printf("\n✅ Completed!\n%s\n", status.Output)
            return
        case status.IsFailed():
            fmt.Printf("\n❌ Failed: %s\n", status.Error)
            return
        default:
            fmt.Printf("⏳ %s...\n", status.Status)
            time.Sleep(backoff)
            if backoff < 30*time.Second {
                backoff *= 2
            }
        }
    }
}

Development

Prerequisites
  • Go 1.22+
  • Podman
  • Make
Commands
# Build
make build

# Run tests
make test

# Run E2E tests (requires Stromboli or Prism)
make test-e2e

# Lint
make lint

# Format
make fmt

# Generate code from OpenAPI spec
make generate
Project Structure
stromboli-go/
├── client.go           # Main client implementation
├── types.go            # Request/response types
├── errors.go           # Error types
├── options.go          # Functional options
├── stream.go           # SSE streaming
├── version.go          # Version info
├── generated/          # Auto-generated code (don't edit)
├── tests/
│   ├── unit/           # Unit tests
│   └── e2e/            # E2E tests
└── scripts/
    └── generate.go     # Code generation script
Running Tests
# Unit tests
make test

# E2E with Prism mock server
make test-e2e

# E2E with real Stromboli
STROMBOLI_URL=http://localhost:8585 STROMBOLI_REAL=1 make test-e2e

License

MIT License - see LICENSE for details.


  • Stromboli — The container orchestration server
  • Documentation — Full Stromboli documentation
  • Issues — Report bugs or request features

Documentation

Overview

Package stromboli provides a Go SDK for the Stromboli API.

Stromboli is a container orchestration service for Claude Code agents, enabling isolated execution of Claude prompts in Podman containers. This SDK provides a clean, idiomatic Go interface to interact with the Stromboli API.

Installation

To install the SDK, use go get:

go get github.com/tomblancdev/stromboli-go

Quick Start

Create a client and execute a prompt:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/tomblancdev/stromboli-go"
)

func main() {
    // Create a new client
    client, err := stromboli.NewClient("http://localhost:8585")
    if err != nil {
        log.Fatal(err)
    }

    // Check API health
    health, err := client.Health(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("API Status: %s (v%s)\n", health.Status, health.Version)
}

Client Configuration

The client can be configured using functional options:

client, err := stromboli.NewClient("http://localhost:8585",
    stromboli.WithTimeout(5*time.Minute),
    stromboli.WithHTTPClient(customHTTPClient),
)
if err != nil {
    log.Fatal(err)
}

Streaming

For real-time output, use the Stream method:

stream, err := client.Stream(ctx, &stromboli.StreamRequest{
    Prompt: "Write a story",
})
if err != nil {
    log.Fatal(err)
}
defer stream.Close()

for stream.Next() {
    fmt.Print(stream.Event().Data)
}

Authentication

For authenticated endpoints, obtain and set a token:

tokens, _ := client.GetToken(ctx, "client-id")
client.SetToken(tokens.AccessToken)

// Now authenticated methods work
validation, _ := client.ValidateToken(ctx)

Error Handling

The SDK provides typed errors for common failure cases:

result, err := client.Run(ctx, req)
if err != nil {
    var apiErr *stromboli.Error
    if errors.As(err, &apiErr) {
        switch apiErr.Code {
        case "NOT_FOUND":
            // Handle not found
        case "TIMEOUT":
            // Handle timeout
        }
    }
}

Architecture

The SDK is built in two layers:

  • Wrapper Layer: Clean, idiomatic Go API with context support and typed error handling (this package)
  • Generated Layer: Auto-generated HTTP client from OpenAPI spec (github.com/tomblancdev/stromboli-go/generated)

Users should only interact with the wrapper layer. The generated layer is an implementation detail and may change between versions.

Thread Safety

The Client is safe for concurrent use by multiple goroutines. Each method call is independent and does not share state.

API Version Compatibility

This SDK version targets Stromboli API v0.4.0-alpha. Compatibility with other API versions is not guaranteed. Use Client.Health to check the server version at runtime.

Index

Constants

View Source
const (
	// RunStatusCompleted indicates successful execution.
	RunStatusCompleted = "completed"

	// RunStatusError indicates execution failed.
	RunStatusError = "error"
)

RunStatus constants for execution results.

View Source
const (
	// StatusOK indicates the service or component is healthy.
	StatusOK = "ok"

	// StatusError indicates the service or component has an error.
	StatusError = "error"
)

HealthStatus constants for convenience.

View Source
const (
	// JobStatusPending indicates the job is queued but not yet started.
	JobStatusPending = "pending"

	// JobStatusRunning indicates the job is currently executing.
	JobStatusRunning = "running"

	// JobStatusCompleted indicates the job completed successfully.
	JobStatusCompleted = "completed"

	// JobStatusFailed indicates the job failed with an error.
	JobStatusFailed = "failed"

	// JobStatusCancelled indicates the job was cancelled.
	JobStatusCancelled = "cancelled"
)

JobStatus constants for async job states.

Use these with [Job.Status]:

if job.Status == stromboli.JobStatusCompleted {
    fmt.Println(job.Output)
}
View Source
const APIVersion = "0.4.0-alpha"

APIVersion is the target Stromboli API version this SDK was built for.

The SDK is tested against this API version and may not work correctly with significantly different API versions. Use Client.Health to check the actual server version at runtime.

View Source
const APIVersionRange = ">=0.4.0-alpha <0.5.0"

APIVersionRange defines the range of Stromboli API versions this SDK is compatible with, using semver constraint syntax.

Examples of constraint syntax:

  • ">=1.0.0 <2.0.0" — versions 1.x.x
  • "^1.2.3" — versions >=1.2.3 and <2.0.0
  • "~1.2.3" — versions >=1.2.3 and <1.3.0

Use IsCompatible or CheckCompatibility to verify a server version.

View Source
const Version = "0.3.1-alpha"

Version is the current SDK version.

This version follows semantic versioning (https://semver.org/). The version is incremented according to the following rules:

  • MAJOR: Breaking changes to the public API
  • MINOR: New features, backwards compatible
  • PATCH: Bug fixes, backwards compatible

Variables

View Source
var (
	// ErrNotFound indicates the requested resource does not exist.
	// Used for jobs, sessions, and other resources without specific not-found errors.
	// HTTP status: 404.
	ErrNotFound = &Error{
		Code:    "NOT_FOUND",
		Message: "resource not found",
		Status:  404,
	}

	// ErrTimeout indicates the request timed out.
	// This can occur for long-running operations or network issues.
	// HTTP status: 408.
	ErrTimeout = &Error{
		Code:    "TIMEOUT",
		Message: "request timed out",
		Status:  408,
	}

	// ErrUnauthorized indicates invalid or missing authentication.
	// HTTP status: 401.
	ErrUnauthorized = &Error{
		Code:    "UNAUTHORIZED",
		Message: "invalid credentials",
		Status:  401,
	}

	// ErrBadRequest indicates invalid request parameters.
	// Check the error message for details about what was invalid.
	// HTTP status: 400.
	ErrBadRequest = &Error{
		Code:    "BAD_REQUEST",
		Message: "invalid request",
		Status:  400,
	}

	// ErrInternal indicates an internal server error.
	// This usually indicates a bug in the Stromboli server.
	// HTTP status: 500.
	ErrInternal = &Error{
		Code:    "INTERNAL",
		Message: "internal server error",
		Status:  500,
	}

	// ErrUnavailable indicates the service is temporarily unavailable.
	// Retry the request after a short delay.
	// HTTP status: 503.
	ErrUnavailable = &Error{
		Code:    "UNAVAILABLE",
		Message: "service temporarily unavailable",
		Status:  503,
	}

	// ErrSecretExists indicates a secret with this name already exists.
	// HTTP status: 409.
	ErrSecretExists = &Error{
		Code:    "SECRET_EXISTS",
		Message: "secret already exists",
		Status:  409,
	}

	// ErrInvalidSecretName indicates the secret name is invalid.
	// HTTP status: 400.
	ErrInvalidSecretName = &Error{
		Code:    "INVALID_SECRET_NAME",
		Message: "invalid secret name",
		Status:  400,
	}

	// ErrImageNotFound indicates the requested image was not found locally.
	// This is distinct from [ErrNotFound] to differentiate between local image
	// lookup failures and other resource not-found errors.
	// Use [Client.PullImage] to fetch the image from a registry.
	// HTTP status: 404.
	ErrImageNotFound = &Error{
		Code:    "IMAGE_NOT_FOUND",
		Message: "image not found",
		Status:  404,
	}

	// ErrImagePullFailed indicates the image pull operation failed.
	// HTTP status: 500.
	ErrImagePullFailed = &Error{
		Code:    "IMAGE_PULL_FAILED",
		Message: "failed to pull image",
		Status:  500,
	}

	// ErrRateLimited indicates too many requests were made.
	// HTTP status: 429.
	//
	// NOTE: The RetryAfter field is not automatically populated because
	// the go-swagger client doesn't expose response headers in error responses.
	// To capture the Retry-After header, use [WithResponseHook] to inspect
	// the response before the error is returned:
	//
	//	var retryAfter time.Duration
	//	client, _ := stromboli.NewClient(url,
	//	    stromboli.WithResponseHook(func(resp *http.Response) {
	//	        if resp.StatusCode == 429 {
	//	            if s := resp.Header.Get("Retry-After"); s != "" {
	//	                if seconds, err := strconv.Atoi(s); err == nil {
	//	                    retryAfter = time.Duration(seconds) * time.Second
	//	                }
	//	            }
	//	        }
	//	    }),
	//	)
	ErrRateLimited = &Error{
		Code:    "RATE_LIMITED",
		Message: "too many requests",
		Status:  429,
	}
)
  • Error Design

Sentinel errors for common error conditions.

Use errors.Is to check for these errors:

if errors.Is(err, stromboli.ErrNotFound) {
    fmt.Println("Resource not found")
}

Error Design

Generic errors (ErrNotFound, ErrTimeout, etc.) are used for most resources. Resource-specific errors exist where the failure mode is domain-specific:

When checking for "not found" errors, use the specific error if available (e.g., ErrImageNotFound for images), or ErrNotFound for other resources.

Functions

func IsCompatible

func IsCompatible(serverVersion string) bool

IsCompatible checks if a server version is compatible with this SDK.

This is a convenience function that returns true if the version falls within APIVersionRange. Use CheckCompatibility for detailed results.

Example:

health, _ := client.Health(ctx)
if !stromboli.IsCompatible(health.Version) {
    log.Printf("Warning: API %s may not be compatible", health.Version)
}

Returns false if the version string cannot be parsed.

func MustBeCompatible

func MustBeCompatible(serverVersion string)

MustBeCompatible panics if the server version is not compatible.

Use this in initialization code where incompatibility should be fatal:

func main() {
    client := stromboli.NewClient(url)
    health, err := client.Health(ctx)
    if err != nil {
        log.Fatal(err)
    }
    stromboli.MustBeCompatible(health.Version)
    // Continue with compatible server...
}

func SetLogger

func SetLogger(l Logger)

SetLogger sets the logger used by the SDK for warnings and debug output. Pass nil to restore the default logger (standard log package). This function is safe for concurrent use.

Example:

// Use a custom logger
stromboli.SetLogger(myLogger)

// Restore default
stromboli.SetLogger(nil)

Types

type AsyncRunResponse

type AsyncRunResponse struct {
	// JobID is the unique identifier for the async job.
	// Use this with [Client.GetJob] to check status.
	// Example: "job-abc123def456"
	JobID string `json:"job_id"`
}

AsyncRunResponse represents the result of starting an async execution.

Use the JobID to poll for completion with Client.GetJob:

async, err := client.RunAsync(ctx, req)
if err != nil {
    log.Fatal(err)
}

// Poll for completion
for {
    job, _ := client.GetJob(ctx, async.JobID)
    if job.Status == "completed" {
        fmt.Println(job.Output)
        break
    }
    time.Sleep(time.Second)
}

type ClaudeOptions

type ClaudeOptions struct {
	// Model specifies the Claude model to use.
	// Use the Model* constants: ModelHaiku, ModelSonnet, ModelOpus.
	// Default: server-configured default (usually sonnet).
	Model Model `json:"model,omitempty"`

	// SessionID enables conversation continuation.
	// Pass a previous response's SessionID to continue the conversation.
	// Example: "550e8400-e29b-41d4-a716-446655440000"
	SessionID string `json:"session_id,omitempty"`

	// Resume continues an existing session (requires SessionID).
	Resume bool `json:"resume,omitempty"`

	// MaxBudgetUSD limits the API cost for this execution.
	// Example: 5.0 means max $5 USD.
	MaxBudgetUSD float64 `json:"max_budget_usd,omitempty"`

	// SystemPrompt replaces the default system prompt entirely.
	// Use AppendSystemPrompt to add to it instead.
	SystemPrompt string `json:"system_prompt,omitempty"`

	// AppendSystemPrompt adds to the default system prompt.
	// Example: "Focus on security best practices"
	AppendSystemPrompt string `json:"append_system_prompt,omitempty"`

	// AllowedTools lists tools Claude can use.
	// Supports patterns like "Bash(git:*)" for git commands only.
	// Example: []string{"Read", "Bash(git:*)", "Edit"}
	AllowedTools []string `json:"allowed_tools,omitempty"`

	// DisallowedTools lists tools Claude cannot use.
	// Example: []string{"Write", "Bash"}
	DisallowedTools []string `json:"disallowed_tools,omitempty"`

	// DangerouslySkipPermissions bypasses all permission checks.
	// Only use in fully sandboxed environments.
	// WARNING: This allows Claude to run any command without confirmation.
	DangerouslySkipPermissions bool `json:"dangerously_skip_permissions,omitempty"`

	// PermissionMode controls how permissions are handled.
	// Values: "default", "acceptEdits", "bypassPermissions", "plan", "dontAsk"
	PermissionMode string `json:"permission_mode,omitempty"`

	// OutputFormat controls the response format.
	// Values: "text", "json", "stream-json"
	OutputFormat string `json:"output_format,omitempty"`

	// JSONSchema specifies a JSON Schema for structured output validation.
	// When set, Claude's output MUST conform to this schema.
	// Requires OutputFormat to be "json" for best results.
	//
	// If the output does not match the schema, the API may return an error
	// or Claude may retry to produce conforming output (behavior depends on
	// the Stromboli server configuration).
	//
	// Example:
	//
	//	&stromboli.ClaudeOptions{
	//	    OutputFormat: "json",
	//	    JSONSchema: `{
	//	        "type": "object",
	//	        "required": ["summary", "score"],
	//	        "properties": {
	//	            "summary": {"type": "string"},
	//	            "score": {"type": "integer", "minimum": 0, "maximum": 100}
	//	        }
	//	    }`,
	//	}
	//
	// See: https://json-schema.org/specification
	JSONSchema string `json:"json_schema,omitempty"`

	// Verbose enables detailed logging.
	Verbose bool `json:"verbose,omitempty"`

	// Debug enables debug mode with optional category filter.
	// Example: "api,hooks"
	Debug string `json:"debug,omitempty"`

	// Continue resumes the most recent conversation in workspace.
	// Ignores SessionID if set.
	Continue bool `json:"continue,omitempty"`

	// Agent specifies a predefined agent configuration.
	// Example: "reviewer"
	Agent string `json:"agent,omitempty"`

	// FallbackModel is used when the primary model is overloaded.
	// Example: "haiku"
	FallbackModel string `json:"fallback_model,omitempty"`

	// AddDirs specifies additional directories for tool access.
	// Example: []string{"/home/user/shared", "/data"}
	AddDirs []string `json:"add_dirs,omitempty"`

	// Agents specifies custom agents definition (JSON object).
	// Example: map[string]interface{}{"reviewer": ...}
	Agents map[string]interface{} `json:"agents,omitempty"`

	// AllowDangerouslySkipPermissions enables bypass as an option without enabling by default.
	AllowDangerouslySkipPermissions bool `json:"allow_dangerously_skip_permissions,omitempty"`

	// Betas specifies beta headers for API requests.
	// Example: []string{"interleaved-thinking-2025-05-14"}
	Betas []string `json:"betas,omitempty"`

	// DisableSlashCommands disables all slash commands/skills.
	DisableSlashCommands bool `json:"disable_slash_commands,omitempty"`

	// Files specifies file resources in format: file_id:path.
	// Example: []string{"abc123:/workspace/file.txt"}
	Files []string `json:"files,omitempty"`

	// ForkSession creates a new session ID when resuming.
	ForkSession bool `json:"fork_session,omitempty"`

	// IncludePartialMessages includes partial message chunks (stream-json only).
	IncludePartialMessages bool `json:"include_partial_messages,omitempty"`

	// InputFormat specifies the input format: text, stream-json.
	// Example: "text"
	InputFormat string `json:"input_format,omitempty"`

	// McpConfigs specifies MCP server config files or JSON strings.
	// Example: []string{"/path/to/mcp.json"}
	McpConfigs []string `json:"mcp_configs,omitempty"`

	// NoPersistence prevents saving session to disk.
	NoPersistence bool `json:"no_persistence,omitempty"`

	// PluginDirs specifies plugin directories.
	// Example: []string{"/home/user/.claude/plugins"}
	PluginDirs []string `json:"plugin_dirs,omitempty"`

	// ReplayUserMessages re-emits user messages on stdout.
	ReplayUserMessages bool `json:"replay_user_messages,omitempty"`

	// SettingSources specifies setting sources to load: user, project, local.
	// Example: []string{"user", "project"}
	SettingSources []string `json:"setting_sources,omitempty"`

	// Settings specifies path to settings JSON file or JSON string.
	// Example: "/path/to/settings.json"
	Settings string `json:"settings,omitempty"`

	// StrictMcpConfig only uses MCP servers from mcp_configs.
	StrictMcpConfig bool `json:"strict_mcp_config,omitempty"`

	// Tools specifies built-in tools ("", "default", or specific names).
	// Example: []string{"Bash", "Read", "Edit"}
	Tools []string `json:"tools,omitempty"`
}

ClaudeOptions configures Claude's behavior during execution.

All fields are optional. Use these to customize the model, set permissions, configure tools, and more.

Example:

&stromboli.ClaudeOptions{
    Model:                      stromboli.ModelSonnet,
    MaxBudgetUSD:               5.0,
    AllowedTools:               []string{"Read", "Bash(git:*)"},
    DangerouslySkipPermissions: true,
}

type ClaudeStatus

type ClaudeStatus struct {
	// Configured indicates whether Claude credentials are set up.
	// When false, execution requests will fail with an authentication error.
	Configured bool `json:"configured"`

	// Message provides additional context about the configuration status.
	// When Configured is true: "Claude is configured"
	// When Configured is false: explains what is missing
	Message string `json:"message"`
}

ClaudeStatus represents the Claude configuration status.

Use Client.ClaudeStatus to check if Claude credentials are configured:

status, err := client.ClaudeStatus(ctx)
if err != nil {
    log.Fatal(err)
}
if status.Configured {
    fmt.Println("Claude is ready!")
} else {
    fmt.Printf("Claude not configured: %s\n", status.Message)
}

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the Stromboli API client.

Client provides a clean, idiomatic Go interface to the Stromboli API. It wraps the auto-generated client with additional features:

  • Context support for cancellation and timeouts
  • Typed errors for common failure cases
  • Simplified request/response types

Create a new client using NewClient:

client, err := stromboli.NewClient("http://localhost:8585")
if err != nil {
    log.Fatal(err)
}

The client is safe for concurrent use by multiple goroutines.

Methods

System:

Execution:

Auth:

Secrets:

func NewClient

func NewClient(baseURL string, opts ...Option) (*Client, error)

NewClient creates a new Stromboli API client.

The baseURL should be the full URL to the Stromboli API, including the protocol and port. Examples:

Returns an error if the URL is invalid or malformed.

Use functional options to customize the client:

client, err := stromboli.NewClient("http://localhost:8585",
    stromboli.WithTimeout(5*time.Minute),
    stromboli.WithHTTPClient(customHTTPClient),
)
if err != nil {
    log.Fatal(err)
}

The returned client is safe for concurrent use.

func (*Client) CancelJob

func (c *Client) CancelJob(ctx context.Context, jobID string) error

CancelJob cancels a pending or running job.

Use this method to stop a job that is no longer needed. Only pending and running jobs can be cancelled. Completed, failed, or already cancelled jobs cannot be cancelled (returns 409 Conflict error).

Example:

err := client.CancelJob(ctx, "job-abc123")
if err != nil {
    if errors.Is(err, stromboli.ErrNotFound) {
        fmt.Println("Job not found")
    } else {
        log.Fatal(err)
    }
}
fmt.Println("Job cancelled successfully")

Cancel a job immediately after starting:

job, _ := client.RunAsync(ctx, req)

// Changed our mind, cancel it
err := client.CancelJob(ctx, job.JobID)
if err != nil {
    log.Fatal(err)
}

func (*Client) ClaudeStatus

func (c *Client) ClaudeStatus(ctx context.Context) (*ClaudeStatus, error)

ClaudeStatus returns the Claude configuration status.

Use this method to check if the Stromboli server has valid Claude credentials configured. If not configured, execution requests will fail.

Example:

status, err := client.ClaudeStatus(ctx)
if err != nil {
    log.Fatalf("Failed to check Claude status: %v", err)
}

if !status.Configured {
    log.Fatalf("Claude is not configured: %s", status.Message)
}

fmt.Println("Claude is ready for execution")

The context can be used to set a timeout or cancel the request:

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
status, err := client.ClaudeStatus(ctx)

func (*Client) ClearToken

func (c *Client) ClearToken()

ClearToken removes the Bearer token from the client.

This is equivalent to calling SetToken("") but more explicit. Use this after Client.Logout to clear local state.

Example:

client.Logout(ctx)
client.ClearToken()

func (*Client) CreateSecret

func (c *Client) CreateSecret(ctx context.Context, req *CreateSecretRequest) error

CreateSecret creates a new Podman secret.

Secrets can be used to securely pass sensitive data (API keys, tokens, etc.) to containers without exposing them in environment variables or command lines.

Example:

err := client.CreateSecret(ctx, &stromboli.CreateSecretRequest{
    Name:  "github-token",
    Value: "ghp_xxxx...",
})
if err != nil {
    log.Fatal(err)
}

Returns ErrSecretExists if a secret with this name already exists:

err := client.CreateSecret(ctx, req)
if errors.Is(err, stromboli.ErrSecretExists) {
    fmt.Println("Secret already exists")
}

func (*Client) DeleteSecret

func (c *Client) DeleteSecret(ctx context.Context, name string) error

DeleteSecret permanently deletes a Podman secret.

WARNING: This action cannot be undone. Secrets currently in use by running containers may cause those containers to fail.

Example:

err := client.DeleteSecret(ctx, "github-token")
if err != nil {
    log.Fatal(err)
}
fmt.Println("Secret deleted")

Returns ErrNotFound if the secret doesn't exist:

err := client.DeleteSecret(ctx, "unknown-secret")
if errors.Is(err, stromboli.ErrNotFound) {
    fmt.Println("Secret not found")
}

func (*Client) DestroySession

func (c *Client) DestroySession(ctx context.Context, sessionID string) error

DestroySession removes a session and all its stored data.

Use this method to clean up old sessions that are no longer needed. This operation is permanent and cannot be undone.

Example:

err := client.DestroySession(ctx, "sess-abc123")
if err != nil {
    if errors.Is(err, stromboli.ErrNotFound) {
        fmt.Println("Session not found")
    } else {
        log.Fatal(err)
    }
}
fmt.Println("Session destroyed")

Bulk cleanup:

sessions, _ := client.ListSessions(ctx)
for _, id := range sessions {
    if err := client.DestroySession(ctx, id); err != nil {
        log.Printf("Failed to destroy %s: %v\n", id, err)
    }
}

func (*Client) GetImage

func (c *Client) GetImage(ctx context.Context, name string) (*Image, error)

GetImage returns detailed information about a specific container image.

This includes all labels, compatibility information, and available tools.

Example:

image, err := client.GetImage(ctx, "python:3.12-slim")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Image: %s\n", image.ID)
fmt.Printf("Compatible: %v\n", image.Compatible)
fmt.Printf("Tools: %v\n", image.Tools)

Returns ErrImageNotFound if the image doesn't exist locally:

image, err := client.GetImage(ctx, "nonexistent:latest")
if errors.Is(err, stromboli.ErrImageNotFound) {
    fmt.Println("Image not found")
}

func (*Client) GetJob

func (c *Client) GetJob(ctx context.Context, jobID string) (*Job, error)

GetJob returns the status and result of an async job.

Use this method to poll for job completion or check the status of a previously started async execution.

Basic polling example:

job, _ := client.RunAsync(ctx, req)

for {
    status, err := client.GetJob(ctx, job.JobID)
    if err != nil {
        log.Fatal(err)
    }

    switch {
    case status.IsCompleted():
        fmt.Println(status.Output)
        return
    case status.IsFailed():
        log.Fatalf("Job failed: %s", status.Error)
    case status.IsRunning():
        fmt.Println("Still running...")
        time.Sleep(2 * time.Second)
    }
}

Returns ErrNotFound if the job doesn't exist:

status, err := client.GetJob(ctx, "invalid-id")
if errors.Is(err, stromboli.ErrNotFound) {
    fmt.Println("Job not found")
}

func (*Client) GetMessage

func (c *Client) GetMessage(ctx context.Context, sessionID, messageID string) (*Message, error)

GetMessage returns a specific message from session history by UUID.

Use this method to retrieve full details about a specific message, including its content, tool calls, and results.

Example:

msg, err := client.GetMessage(ctx, "sess-abc123", "msg-uuid-456")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Role: %s\n", msg.Type)
fmt.Printf("Content: %v\n", msg.Content)

func (*Client) GetMessages

func (c *Client) GetMessages(ctx context.Context, sessionID string, opts *GetMessagesOptions) (*MessagesResponse, error)

GetMessages returns paginated conversation history for a session.

Use this method to retrieve past messages from a session, including user prompts, assistant responses, tool calls, and results.

Basic usage:

messages, err := client.GetMessages(ctx, "sess-abc123", nil)
if err != nil {
    log.Fatal(err)
}

for _, msg := range messages.Messages {
    fmt.Printf("[%s] %s\n", msg.Type, msg.UUID)
}

With pagination:

messages, _ := client.GetMessages(ctx, "sess-abc123", &stromboli.GetMessagesOptions{
    Limit:  50,
    Offset: 100,
})

if messages.HasMore {
    // Fetch next page
    nextPage, _ := client.GetMessages(ctx, "sess-abc123", &stromboli.GetMessagesOptions{
        Limit:  50,
        Offset: messages.Offset + messages.Limit,
    })
}

func (*Client) GetSecret

func (c *Client) GetSecret(ctx context.Context, name string) (*Secret, error)

GetSecret retrieves metadata for a specific secret.

For security, the actual secret value is never returned - only the ID, name, and creation time.

Example:

secret, err := client.GetSecret(ctx, "github-token")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Secret %s created at %s\n", secret.Name, secret.CreatedAt)

Returns ErrNotFound if the secret doesn't exist:

secret, err := client.GetSecret(ctx, "unknown-secret")
if errors.Is(err, stromboli.ErrNotFound) {
    fmt.Println("Secret not found")
}

func (*Client) GetToken

func (c *Client) GetToken(ctx context.Context, clientID string) (*TokenResponse, error)

GetToken obtains JWT tokens using a client ID.

Use this method to authenticate with the Stromboli API. The returned tokens can be used for subsequent authenticated requests.

Example:

tokens, err := client.GetToken(ctx, "my-client-id")
if err != nil {
    log.Fatal(err)
}

// Set the token for future requests
client.SetToken(tokens.AccessToken)

// Token expires in tokens.ExpiresIn seconds
fmt.Printf("Token expires in %d seconds\n", tokens.ExpiresIn)

func (*Client) Health

func (c *Client) Health(ctx context.Context) (*HealthResponse, error)

Health returns the health status of the Stromboli API.

Use this method to:

  • Check if the API is reachable and healthy
  • Verify the server version
  • Check the status of individual components (e.g., Podman)

Example:

health, err := client.Health(ctx)
if err != nil {
    log.Fatalf("API is unreachable: %v", err)
}

if !health.IsHealthy() {
    for _, c := range health.Components {
        if !c.IsHealthy() {
            log.Printf("Component %s is unhealthy: %s", c.Name, c.Error)
        }
    }
}

fmt.Printf("API v%s is healthy\n", health.Version)

The context can be used to set a timeout or cancel the request:

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
health, err := client.Health(ctx)

func (*Client) ListImages

func (c *Client) ListImages(ctx context.Context) ([]*Image, error)

ListImages returns all local container images sorted by compatibility rank.

Images are ranked by their compatibility with Stromboli:

  • Rank 1-2: Verified compatible (have required tools)
  • Rank 3: Standard glibc (compatible)
  • Rank 4: Incompatible (Alpine/musl)

Example:

images, err := client.ListImages(ctx)
if err != nil {
    log.Fatal(err)
}

for _, img := range images {
    fmt.Printf("%s:%s (rank %d, compatible: %v)\n",
        img.Repository, img.Tag, img.CompatibilityRank, img.Compatible)
}

func (*Client) ListJobs

func (c *Client) ListJobs(ctx context.Context) ([]*Job, error)

ListJobs returns all async jobs.

Use this method to get an overview of all jobs, their status, and when they were created. The list includes pending, running, completed, failed, and cancelled jobs.

Example:

jobs, err := client.ListJobs(ctx)
if err != nil {
    log.Fatal(err)
}

for _, job := range jobs {
    fmt.Printf("%s: %s (created: %s)\n", job.ID, job.Status, job.CreatedAt)
}

Filter by status:

jobs, _ := client.ListJobs(ctx)
for _, job := range jobs {
    if job.IsRunning() {
        fmt.Printf("Job %s is still running\n", job.ID)
    }
}

func (*Client) ListSecrets

func (c *Client) ListSecrets(ctx context.Context) ([]*Secret, error)

ListSecrets returns all available Podman secrets.

These secrets can be injected into container execution environments using [PodmanOptions.SecretsEnv].

Example:

secrets, err := client.ListSecrets(ctx)
if err != nil {
    log.Fatal(err)
}

for _, s := range secrets {
    fmt.Printf("Secret: %s (created: %s)\n", s.Name, s.CreatedAt)
}

Using secrets in execution:

secrets, _ := client.ListSecrets(ctx)

result, _ := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "Use my GitHub token to list repos",
    Podman: &stromboli.PodmanOptions{
        SecretsEnv: map[string]string{
            "GITHUB_TOKEN": secrets[0].Name, // Use first available secret
        },
    },
})

func (*Client) ListSessions

func (c *Client) ListSessions(ctx context.Context) ([]string, error)

ListSessions returns all existing session IDs.

Sessions are created automatically when running Claude with a new conversation. Use this method to list all available sessions for resumption or cleanup.

Example:

sessionIDs, err := client.ListSessions(ctx)
if err != nil {
    log.Fatal(err)
}

for _, id := range sessionIDs {
    fmt.Printf("Session: %s\n", id)
}

To continue a specific session, use the session ID with [ClaudeOptions.SessionID]:

result, _ := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "What did we discuss earlier?",
    Claude: &stromboli.ClaudeOptions{
        SessionID: sessionIDs[0],
        Resume:    true,
    },
})

func (*Client) Logout

func (c *Client) Logout(ctx context.Context) (*LogoutResponse, error)

Logout invalidates the current access token.

After calling this method, the token will no longer be accepted by the API. This method requires a valid token to be set using Client.SetToken.

Example:

client.SetToken(accessToken)

result, err := client.Logout(ctx)
if err != nil {
    log.Fatal(err)
}

if result.Success {
    fmt.Println("Successfully logged out")
    client.SetToken("") // Clear the token
}

func (*Client) PullImage

func (c *Client) PullImage(ctx context.Context, req *PullImageRequest) (*PullImageResponse, error)

PullImage pulls a container image from a registry.

This operation may take some time for large images.

Example:

result, err := client.PullImage(ctx, &stromboli.PullImageRequest{
    Image:    "python:3.12-slim",
    Platform: "linux/amd64",
})
if err != nil {
    log.Fatal(err)
}

if result.Success {
    fmt.Printf("Pulled image %s (ID: %s)\n", result.Image, result.ImageID)
}

func (*Client) RefreshToken

func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error)

RefreshToken obtains a new access token using a refresh token.

Use this method when your access token has expired. The refresh token has a longer lifetime and can be used to obtain new access tokens.

Example:

// When access token expires, use refresh token
newTokens, err := client.RefreshToken(ctx, tokens.RefreshToken)
if err != nil {
    // Refresh token may also be expired, need to re-authenticate
    log.Fatal(err)
}

client.SetToken(newTokens.AccessToken)

func (*Client) Run

func (c *Client) Run(ctx context.Context, req *RunRequest) (*RunResponse, error)

Run executes Claude synchronously and waits for the result.

This method blocks until Claude completes execution or an error occurs. For long-running tasks, consider using Client.RunAsync instead.

Basic usage:

result, err := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "Hello, Claude!",
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(result.Output)

With configuration:

result, err := client.Run(ctx, &stromboli.RunRequest{
    Prompt:  "Review this code for security issues",
    Workdir: "/workspace",
    Claude: &stromboli.ClaudeOptions{
        Model:        stromboli.ModelSonnet,
        MaxBudgetUSD: 5.0,
        AllowedTools: []string{"Read", "Glob", "Grep"},
    },
    Podman: &stromboli.PodmanOptions{
        Memory:  "2g",
        Timeout: "10m",
        Volumes: []string{"/home/user/project:/workspace:ro"},
    },
})

Continuing a conversation:

// First request
result1, _ := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "Remember: my favorite color is blue",
})

// Continue the conversation
result2, _ := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "What's my favorite color?",
    Claude: &stromboli.ClaudeOptions{
        SessionID: result1.SessionID,
        Resume:    true,
    },
})

Timeout Behavior

The effective request timeout is determined by the shorter of:

For long-running tasks, either increase the client timeout or use Client.RunAsync instead.

The context can be used for cancellation:

ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
result, err := client.Run(ctx, req)

func (*Client) RunAsync

func (c *Client) RunAsync(ctx context.Context, req *RunRequest) (*AsyncRunResponse, error)

RunAsync starts Claude execution asynchronously and returns a job ID.

Use this method for long-running tasks. Poll the job status with Client.GetJob or configure a webhook to be notified on completion.

Basic usage:

job, err := client.RunAsync(ctx, &stromboli.RunRequest{
    Prompt: "Analyze this large codebase",
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Job started: %s\n", job.JobID)

With webhook notification:

job, err := client.RunAsync(ctx, &stromboli.RunRequest{
    Prompt:     "Review all files in the project",
    WebhookURL: "https://example.com/webhook",
})

Polling for completion:

job, _ := client.RunAsync(ctx, req)

for {
    status, err := client.GetJob(ctx, job.JobID)
    if err != nil {
        log.Fatal(err)
    }

    switch status.Status {
    case "completed":
        fmt.Println(status.Output)
        return
    case "failed":
        log.Fatalf("Job failed: %s", status.Error)
    case "running":
        fmt.Println("Still running...")
        time.Sleep(2 * time.Second)
    }
}

func (*Client) SearchImages

func (c *Client) SearchImages(ctx context.Context, opts *SearchImagesOptions) ([]*ImageSearchResult, error)

SearchImages searches container registries for images matching the query.

Returns results from Docker Hub and other configured registries.

Example:

results, err := client.SearchImages(ctx, &stromboli.SearchImagesOptions{
    Query: "python",
    Limit: 10,
})
if err != nil {
    log.Fatal(err)
}

for _, r := range results {
    fmt.Printf("%s: %s (stars: %d, official: %v)\n",
        r.Name, r.Description, r.Stars, r.Official)
}

func (*Client) SetToken

func (c *Client) SetToken(token string)

SetToken sets the Bearer token for authenticated requests.

This token is used for endpoints that require authentication, such as Client.ValidateToken and Client.Logout. SetToken is safe for concurrent use.

Token Validation

Tokens are validated to prevent HTTP header injection attacks. If a token contains control characters (CR, LF, or other characters < 0x20), the token is rejected and a warning is logged. The previous token remains unchanged. This is a security measure - valid JWT tokens never contain these characters.

To check if a token was accepted, call [Client.getToken] after setting, or use Client.ValidateToken to verify with the server.

Empty Token

Passing an empty string clears the token. Use Client.ClearToken for a more explicit way to remove the token.

Example:

tokens, _ := client.GetToken(ctx, "my-client-id")
client.SetToken(tokens.AccessToken)

// Now authenticated endpoints will work
validation, _ := client.ValidateToken(ctx)

func (*Client) Stream

func (c *Client) Stream(ctx context.Context, req *StreamRequest) (*Stream, error)

Stream executes Claude and streams output in real-time.

This method connects to the SSE (Server-Sent Events) endpoint and returns a Stream that yields events as they arrive.

Timeout Behavior

WARNING: The client timeout (WithTimeout) does NOT apply to streaming requests. If no context deadline is set and the server stops responding, this method may block indefinitely. Always use context.WithTimeout:

ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
stream, err := client.Stream(ctx, req)

The timeout behavior differs from regular requests because streams are designed for long-running connections where data arrives incrementally.

Basic Usage

stream, err := client.Stream(ctx, &stromboli.StreamRequest{
    Prompt: "Write a haiku about Go programming",
})
if err != nil {
    log.Fatal(err)
}
defer stream.Close()

for stream.Next() {
    fmt.Print(stream.Event().Data)
}

if err := stream.Err(); err != nil {
    log.Fatal(err)
}

Channel Iteration

stream, _ := client.Stream(ctx, req)
defer stream.Close()

for event := range stream.Events() {
    fmt.Print(event.Data)
}

Continuing a Conversation

// First interaction
stream1, _ := client.Stream(ctx, &stromboli.StreamRequest{
    Prompt: "My name is Alice",
})
// ... consume stream1 ...
sessionID := "..." // Get from previous response

// Continue conversation
stream2, _ := client.Stream(ctx, &stromboli.StreamRequest{
    Prompt:    "What's my name?",
    SessionID: sessionID,
})

func (*Client) ValidateToken

func (c *Client) ValidateToken(ctx context.Context) (*TokenValidation, error)

ValidateToken validates the current access token and returns its claims.

This method requires a valid token to be set using Client.SetToken.

Example:

client.SetToken(accessToken)

validation, err := client.ValidateToken(ctx)
if err != nil {
    log.Fatal(err)
}

if validation.Valid {
    fmt.Printf("Token valid for subject: %s\n", validation.Subject)
    fmt.Printf("Expires at: %d\n", validation.ExpiresAt)
}

type CompatibilityResult

type CompatibilityResult struct {
	// Status is the compatibility status.
	Status CompatibilityStatus

	// ServerVersion is the version reported by the server.
	ServerVersion string

	// SDKVersion is this SDK's version.
	SDKVersion string

	// TargetAPIVersion is the API version this SDK was built for.
	TargetAPIVersion string

	// SupportedRange is the range of API versions this SDK supports.
	SupportedRange string

	// Message is a human-readable description of the result.
	Message string
}

CompatibilityResult contains detailed information about API compatibility.

func CheckCompatibility

func CheckCompatibility(serverVersion string) *CompatibilityResult

CheckCompatibility performs a detailed compatibility check between the server version and this SDK.

Example:

health, _ := client.Health(ctx)
result := stromboli.CheckCompatibility(health.Version)

switch result.Status {
case stromboli.Compatible:
    fmt.Println("Server is compatible")
case stromboli.Incompatible:
    fmt.Printf("Warning: %s\n", result.Message)
case stromboli.Unknown:
    fmt.Printf("Could not determine compatibility: %s\n", result.Message)
}

func (*CompatibilityResult) IsCompatible

func (r *CompatibilityResult) IsCompatible() bool

IsCompatible returns true if the status indicates compatibility.

type CompatibilityStatus

type CompatibilityStatus int

CompatibilityStatus represents the result of a version compatibility check.

const (
	// Compatible means the API version is within the supported range.
	Compatible CompatibilityStatus = iota

	// Incompatible means the API version is outside the supported range.
	Incompatible

	// Unknown means the version could not be parsed.
	Unknown
)

func (CompatibilityStatus) String

func (s CompatibilityStatus) String() string

String returns a human-readable representation of the status.

type ComponentHealth

type ComponentHealth struct {
	// Name is the component identifier.
	// Example: "podman".
	Name string `json:"name"`

	// Status indicates the component health.
	// Values: "ok" (healthy) or "error" (unhealthy).
	Status string `json:"status"`

	// Error contains the error message when Status is "error".
	// Empty when Status is "ok".
	Error string `json:"error,omitempty"`
}

ComponentHealth represents the health status of an individual component.

Stromboli checks the following components:

  • "podman": Container runtime availability
  • Additional components may be added in future versions

func (*ComponentHealth) IsHealthy

func (c *ComponentHealth) IsHealthy() bool

IsHealthy returns true if the component status is "ok".

type CrashInfo

type CrashInfo struct {
	// Reason is a human-readable description of why the job crashed.
	// Example: "Container OOM killed", "Timeout exceeded"
	Reason string `json:"reason,omitempty"`

	// ExitCode is the container exit code (if available).
	// Common values: 137 (OOM), 143 (SIGTERM), 1 (general error)
	ExitCode int64 `json:"exit_code,omitempty"`

	// PartialOutput contains any output captured before the crash.
	// This can help debug what the job was doing when it crashed.
	PartialOutput string `json:"partial_output,omitempty"`

	// Signal is the signal that killed the process (if applicable).
	// Examples: "SIGSEGV", "SIGKILL", "SIGTERM"
	Signal string `json:"signal,omitempty"`

	// TaskCompleted indicates whether the task appeared to complete before crashing.
	TaskCompleted bool `json:"task_completed,omitempty"`
}

CrashInfo contains details about a job crash.

This is populated when a job terminates unexpectedly due to container issues, OOM errors, or other infrastructure problems.

type CreateSecretRequest

type CreateSecretRequest struct {
	// Name is the secret name (required).
	// Must be unique among existing secrets.
	// Example: "github-token"
	Name string `json:"name"`

	// Value is the secret data (required).
	// This value is stored securely and never returned by the API.
	// Example: "ghp_xxxx..."
	Value string `json:"value"`
}

CreateSecretRequest represents a request to create a new Podman secret.

Use with Client.CreateSecret:

err := client.CreateSecret(ctx, &stromboli.CreateSecretRequest{
    Name:  "github-token",
    Value: "ghp_xxxx...",
})

type EnvironmentConfig

type EnvironmentConfig struct {
	// Type of environment: "" (default single container) or "compose".
	// Example: "compose"
	Type string `json:"type,omitempty"`

	// Path to compose file (required when Type="compose").
	// Must be an absolute path ending in .yml or .yaml.
	// Example: "/home/user/project/docker-compose.yml"
	Path string `json:"path,omitempty"`

	// Service name where Claude will run (required when Type="compose").
	// Example: "dev"
	Service string `json:"service,omitempty"`

	// BuildTimeout is the optional build timeout override for compose.
	// If not specified, uses server default (10m).
	// Examples: "15m", "30m"
	BuildTimeout string `json:"build_timeout,omitempty"`
}

EnvironmentConfig specifies a compose-based multi-service environment.

When set, the agent runs inside the specified service of a Docker Compose stack instead of a standalone container. This allows running Claude in complex multi-container environments.

Example:

&stromboli.EnvironmentConfig{
    Type:    "compose",
    Path:    "/home/user/project/docker-compose.yml",
    Service: "dev",
}

type Error

type Error struct {
	// Code is a machine-readable error code.
	// Common values: NOT_FOUND, TIMEOUT, UNAUTHORIZED, BAD_REQUEST, INTERNAL.
	Code string

	// Message is a human-readable error description.
	Message string

	// Status is the HTTP status code returned by the API.
	// Zero if the error occurred before receiving a response.
	Status int

	// Cause is the underlying error, if any.
	// Use errors.Unwrap or errors.Is to inspect the cause chain.
	Cause error

	// RetryAfter indicates how long to wait before retrying (for 429 responses).
	// Zero if no Retry-After header was provided or not applicable.
	RetryAfter time.Duration
}

Error represents an error returned by the Stromboli API.

Error implements the standard error interface and supports error wrapping via the Unwrap method. Use errors.As to check for specific error types:

result, err := client.Run(ctx, req)
if err != nil {
    var apiErr *stromboli.Error
    if errors.As(err, &apiErr) {
        fmt.Printf("API error %s: %s\n", apiErr.Code, apiErr.Message)
    }
}

Common error codes include:

  • NOT_FOUND: The requested resource does not exist
  • TIMEOUT: The request timed out
  • UNAUTHORIZED: Invalid or missing authentication
  • BAD_REQUEST: Invalid request parameters
  • INTERNAL: Internal server error

func (*Error) Error

func (e *Error) Error() string

Error returns a string representation of the error.

The format is "stromboli: CODE: message" or "stromboli: CODE: message: cause" if there is an underlying error.

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether the target error matches this error.

Two errors match if they have the same Code. The Status field is NOT compared, so errors.Is(err, ErrNotFound) matches any NOT_FOUND error regardless of the HTTP status code. This allows sentinel errors to match all instances of that error type.

Example:

if errors.Is(err, stromboli.ErrNotFound) {
    // Handles any NOT_FOUND error (404, or otherwise)
}

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying error cause.

This allows using errors.Is and errors.As to inspect the error chain.

type GetMessagesOptions

type GetMessagesOptions struct {
	// Limit is the maximum number of messages to return (default: 50, max: 200).
	Limit int64 `json:"limit,omitempty"`

	// Offset is the number of messages to skip (for pagination).
	Offset int64 `json:"offset,omitempty"`
}

GetMessagesOptions configures the pagination for Client.GetMessages.

Example:

messages, _ := client.GetMessages(ctx, "sess-abc123", &stromboli.GetMessagesOptions{
    Limit:  50,
    Offset: 100,
})

type HealthResponse

type HealthResponse struct {
	// Name is the service name, typically "stromboli".
	Name string `json:"name"`

	// Status indicates the overall health status.
	// Values: "ok" (healthy) or "error" (unhealthy).
	Status string `json:"status"`

	// Version is the Stromboli server version.
	// Example: "0.3.0-alpha".
	Version string `json:"version"`

	// Components lists the health status of individual components.
	// Check this to identify which component is failing when Status is "error".
	Components []ComponentHealth `json:"components"`
}

HealthResponse represents the health status of the Stromboli API.

Use Client.Health to retrieve the current health status:

health, err := client.Health(ctx)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Status: %s, Version: %s\n", health.Status, health.Version)

func (*HealthResponse) IsHealthy

func (h *HealthResponse) IsHealthy() bool

IsHealthy returns true if the overall status is "ok".

Example:

health, _ := client.Health(ctx)
if !health.IsHealthy() {
    log.Println("API is unhealthy!")
}

type Image

type Image struct {
	// ID is the image ID (usually sha256:...).
	// Example: "sha256:abc123def456"
	ID string `json:"id,omitempty"`

	// Repository is the image repository name.
	// Example: "python"
	Repository string `json:"repository,omitempty"`

	// Tag is the image tag.
	// Example: "3.12-slim"
	Tag string `json:"tag,omitempty"`

	// Size is the image size in bytes.
	// Example: 125000000
	Size int64 `json:"size,omitempty"`

	// Created is when the image was created (RFC3339 format).
	// Example: "2024-01-15T10:30:00Z"
	Created string `json:"created,omitempty"`

	// Description is a human-readable description of the image.
	// Example: "Python development image"
	Description string `json:"description,omitempty"`

	// Compatible indicates if the image is compatible with Stromboli.
	// Images with glibc are compatible; Alpine/musl images are not.
	Compatible bool `json:"compatible,omitempty"`

	// CompatibilityRank indicates the image's compatibility level.
	// 1-2: Verified compatible, 3: Standard glibc, 4: Incompatible (Alpine/musl)
	CompatibilityRank int64 `json:"compatibility_rank,omitempty"`

	// HasClaudeCLI indicates if the image has Claude CLI pre-installed.
	HasClaudeCLI bool `json:"has_claude_cli,omitempty"`

	// Tools lists tools available in the image.
	// Example: []string{"python", "pip", "git"}
	Tools []string `json:"tools,omitempty"`
}

Image represents a local container image with compatibility information.

Use Client.ListImages to list all available images:

images, err := client.ListImages(ctx)
for _, img := range images {
    fmt.Printf("%s:%s (rank %d)\n", img.Repository, img.Tag, img.CompatibilityRank)
}

func (*Image) CreatedTime

func (i *Image) CreatedTime() time.Time

CreatedTime parses Created as time.Time. Returns zero time if Created is empty or parsing fails.

NOTE: Parsing errors are silently ignored. If you need to validate the timestamp format, use time.Parse(time.RFC3339, i.Created) directly.

type ImageSearchResult

type ImageSearchResult struct {
	// Name is the image name.
	// Example: "python"
	Name string `json:"name,omitempty"`

	// Description is the image description from the registry.
	// Example: "Python is an interpreted programming language"
	Description string `json:"description,omitempty"`

	// Stars is the number of stars on the registry.
	// Example: 8500
	Stars int64 `json:"stars,omitempty"`

	// Official indicates if this is an official image.
	Official bool `json:"official,omitempty"`

	// Automated indicates if this image is automatically built.
	Automated bool `json:"automated,omitempty"`

	// Index is the registry index (e.g., "docker.io").
	// Example: "docker.io"
	Index string `json:"index,omitempty"`
}

ImageSearchResult represents a search result from a container registry.

Use Client.SearchImages to search registries:

results, err := client.SearchImages(ctx, &stromboli.SearchImagesOptions{
    Query: "python",
    Limit: 10,
})
for _, r := range results {
    fmt.Printf("%s: %s (stars: %d)\n", r.Name, r.Description, r.Stars)
}

type Job

type Job struct {
	// ID is the unique job identifier.
	// Example: "job-abc123def456"
	ID string `json:"id"`

	// Status indicates the current job state.
	// Values: "pending", "running", "completed", "failed", "cancelled"
	Status string `json:"status"`

	// Output contains Claude's response when Status is "completed".
	Output string `json:"output,omitempty"`

	// Error contains the error message when Status is "failed".
	Error string `json:"error,omitempty"`

	// SessionID can be used to continue this conversation.
	// Pass this to RunRequest.Claude.SessionID for follow-up requests.
	SessionID string `json:"session_id,omitempty"`

	// CreatedAt is when the job was created (RFC3339 format).
	// Example: "2024-01-15T10:30:00Z"
	CreatedAt string `json:"created_at,omitempty"`

	// UpdatedAt is when the job was last updated (RFC3339 format).
	// Example: "2024-01-15T10:31:00Z"
	UpdatedAt string `json:"updated_at,omitempty"`

	// CrashInfo contains crash details if the job crashed.
	CrashInfo *CrashInfo `json:"crash_info,omitempty"`
}

Job represents the status and result of an async job.

Use Client.GetJob to retrieve job status, or Client.ListJobs to list all jobs:

job, err := client.GetJob(ctx, "job-abc123")
if err != nil {
    log.Fatal(err)
}

switch job.Status {
case stromboli.JobStatusCompleted:
    fmt.Println(job.Output)
case stromboli.JobStatusRunning:
    fmt.Println("Still running...")
case stromboli.JobStatusFailed:
    fmt.Printf("Failed: %s\n", job.Error)
}

func (*Job) CreatedAtTime

func (j *Job) CreatedAtTime() time.Time

CreatedAtTime parses CreatedAt as time.Time. Returns zero time if CreatedAt is empty or parsing fails.

NOTE: Parsing errors are silently ignored. If you need to validate the timestamp format, use time.Parse(time.RFC3339, j.CreatedAt) directly.

func (*Job) IsCancelled

func (j *Job) IsCancelled() bool

IsCancelled returns true if the job was cancelled.

func (*Job) IsCompleted

func (j *Job) IsCompleted() bool

IsCompleted returns true if the job completed successfully.

func (*Job) IsFailed

func (j *Job) IsFailed() bool

IsFailed returns true if the job failed.

func (*Job) IsPending

func (j *Job) IsPending() bool

IsPending returns true if the job is pending (queued but not yet started).

func (*Job) IsRunning

func (j *Job) IsRunning() bool

IsRunning returns true if the job is still running.

func (*Job) UpdatedAtTime

func (j *Job) UpdatedAtTime() time.Time

UpdatedAtTime parses UpdatedAt as time.Time. Returns zero time if UpdatedAt is empty or parsing fails.

NOTE: Parsing errors are silently ignored. If you need to validate the timestamp format, use time.Parse(time.RFC3339, j.UpdatedAt) directly.

type LifecycleHooks

type LifecycleHooks struct {
	// OnCreateCommand runs after container creation, before Claude starts (first run only).
	// Commands are executed sequentially via "podman exec".
	// Example: []string{"pip install -r requirements.txt"}
	OnCreateCommand []string `json:"on_create_command,omitempty"`

	// PostCreate runs after OnCreateCommand completes (first run only).
	// Commands are executed sequentially via "podman exec".
	// Example: []string{"npm run setup"}
	PostCreate []string `json:"post_create,omitempty"`

	// PostStart runs after container starts (every run, including continues).
	// Commands are executed sequentially via "podman exec".
	// Example: []string{"redis-server --daemonize yes"}
	PostStart []string `json:"post_start,omitempty"`

	// HooksTimeout is the maximum duration for all hooks combined.
	// If not specified, hooks run with the container's timeout.
	// Examples: "5m", "30s"
	HooksTimeout string `json:"hooks_timeout,omitempty"`
}

LifecycleHooks configures commands to run at specific container lifecycle stages.

Use these hooks to set up the container environment before Claude starts, such as installing dependencies, starting background services, etc.

Example:

&stromboli.LifecycleHooks{
    OnCreateCommand: []string{"pip install -r requirements.txt"},
    PostStart:       []string{"redis-server --daemonize yes"},
    HooksTimeout:    "5m",
}

type Logger

type Logger interface {
	Printf(format string, v ...interface{})
}

Logger is the interface used for SDK logging. Implement this interface to customize log output.

type LogoutResponse

type LogoutResponse struct {
	// Success indicates whether the logout was successful.
	Success bool `json:"success"`

	// Message provides additional context about the logout.
	Message string `json:"message"`
}

LogoutResponse represents the result of invalidating a token.

Use Client.Logout to invalidate the current token:

result, err := client.Logout(ctx)
if err != nil {
    log.Fatal(err)
}
if result.Success {
    fmt.Println("Logged out successfully")
}

type Message

type Message struct {
	// UUID is the unique message identifier.
	// Example: "92242819-b7d1-48d4-b023-6134c3e9f63a"
	UUID string `json:"uuid,omitempty"`

	// Type indicates the message type.
	// Values: "user", "assistant", "queue-operation"
	Type string `json:"type,omitempty"`

	// ParentUUID is the parent message UUID for threading.
	ParentUUID string `json:"parent_uuid,omitempty"`

	// SessionID is the session this message belongs to.
	SessionID string `json:"session_id,omitempty"`

	// Cwd is the working directory at time of message.
	// Example: "/workspace"
	Cwd string `json:"cwd,omitempty"`

	// GitBranch is the git branch at time of message.
	// Example: "main"
	GitBranch string `json:"git_branch,omitempty"`

	// PermissionMode is the permission mode active for this message.
	// Example: "bypassPermissions"
	PermissionMode string `json:"permission_mode,omitempty"`

	// Timestamp is when the message was created (RFC3339 format).
	Timestamp string `json:"timestamp,omitempty"`

	// Version is the Claude Code version that created this message.
	Version string `json:"version,omitempty"`

	// Content contains the message content (text, tool calls, etc.).
	// The structure varies by message type:
	//   - For "user" messages: string or []ContentBlock
	//   - For "assistant" messages: []ContentBlock with text and tool_use
	//
	// Use type assertions or json.Marshal/Unmarshal to work with this field.
	//
	// Example:
	//
	//	// Check if content is a simple string
	//	if text, ok := msg.Content.(string); ok {
	//	    fmt.Println(text)
	//	}
	//
	//	// For complex content, marshal and unmarshal
	//	data, _ := json.Marshal(msg.Content)
	//	var blocks []map[string]interface{}
	//	json.Unmarshal(data, &blocks)
	Content interface{} `json:"content,omitempty"`

	// ToolResult contains tool use results (for tool_result messages).
	// The structure is typically:
	//   - ToolUseID: string - The ID of the tool use this result responds to
	//   - Content: string or []ContentBlock - The result data
	//   - IsError: bool - Whether this result represents an error
	//
	// Use type assertions or json.Marshal/Unmarshal to work with this field.
	ToolResult interface{} `json:"tool_result,omitempty"`
}

Message represents a single message from session history.

Messages can be user prompts, assistant responses, or tool interactions. Use Client.GetMessages to list messages or Client.GetMessage to get a specific message by UUID.

func (*Message) ContentAsBlocks

func (m *Message) ContentAsBlocks() (blocks []map[string]interface{}, skipped int, ok bool)

ContentAsBlocks returns the content as a slice of maps if it contains content blocks. Returns nil and false if content is not in block format.

WARNING: Non-map entries in the content array are skipped. The returned int indicates how many entries were skipped, allowing callers to detect data loss. If skipped > 0, some content was not map-typed and was omitted from results.

The ok return value indicates whether Content was in array format ([]interface{}), NOT whether any blocks were found. An empty content array returns ok=true with an empty blocks slice. Use ok=false to detect non-array content formats.

For more precise typing, use json.Marshal/Unmarshal:

data, _ := json.Marshal(msg.Content)
var blocks []YourBlockType
json.Unmarshal(data, &blocks)

func (*Message) ContentAsString

func (m *Message) ContentAsString() (string, bool)

ContentAsString returns the content as a string if it is a simple string message. Returns empty string and false if content is not a string.

Example:

if text, ok := msg.ContentAsString(); ok {
    fmt.Println(text)
}

func (*Message) TimestampTime

func (m *Message) TimestampTime() time.Time

TimestampTime parses Timestamp as time.Time. Returns zero time if Timestamp is empty or parsing fails.

NOTE: Parsing errors are silently ignored. If you need to validate the timestamp format, use time.Parse(time.RFC3339, m.Timestamp) directly.

type MessagesResponse

type MessagesResponse struct {
	// Messages is the list of messages in this page.
	Messages []*Message `json:"messages"`

	// Total is the total number of messages in the session.
	Total int64 `json:"total"`

	// Limit is the maximum messages per page (requested or default).
	Limit int64 `json:"limit"`

	// Offset is the number of messages skipped.
	Offset int64 `json:"offset"`

	// HasMore indicates if there are more messages to fetch.
	HasMore bool `json:"has_more"`
}

MessagesResponse represents a paginated list of session messages.

Use Client.GetMessages to retrieve messages from a session:

resp, _ := client.GetMessages(ctx, "sess-abc123", nil)
for _, msg := range resp.Messages {
    fmt.Printf("[%s] %s\n", msg.Type, msg.UUID)
}

if resp.HasMore {
    // Fetch more messages...
}

type Model

type Model string

Model represents a Claude model identifier.

The SDK provides constants for common models (ModelHaiku, ModelSonnet, ModelOpus). For newer models not yet added to the SDK, you can cast any string to Model:

customModel := stromboli.Model("claude-3-5-sonnet-20241022")

Model values are passed directly to the API, so you can use any model identifier supported by the Stromboli server.

const (
	// ModelHaiku is the fastest and most cost-effective model.
	// Best for simple tasks, quick responses, and high-volume use cases.
	ModelHaiku Model = "haiku"

	// ModelSonnet is the balanced model for most use cases.
	// Good balance of speed, capability, and cost.
	ModelSonnet Model = "sonnet"

	// ModelOpus is the most capable model.
	// Best for complex reasoning, nuanced tasks, and highest quality output.
	ModelOpus Model = "opus"
)

Model constants for Claude model selection.

Use these with [ClaudeOptions.Model]:

&stromboli.ClaudeOptions{
    Model: stromboli.ModelHaiku,
}

func (Model) String

func (m Model) String() string

String returns the string representation of the Model.

type Option

type Option func(*Client)

Option configures a Client.

Options are passed to NewClient to customize the client behavior. Multiple options can be combined:

client, err := stromboli.NewClient("http://localhost:8585",
    stromboli.WithTimeout(5*time.Minute),
    stromboli.WithHTTPClient(customHTTPClient),
)

Options are applied in order, so later options override earlier ones.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient sets a custom HTTP client for making requests.

Use this option to customize transport settings like:

  • TLS configuration
  • Proxy settings
  • Connection pooling
  • Custom transports (e.g., for testing)

The provided client's Timeout field is ignored in favor of WithTimeout. Use WithTimeout to control request timeouts.

Passing nil logs a warning and is ignored (the default client is retained). This is typically a programmer error; check for nil before calling.

Default: A new http.Client with cloned http.DefaultTransport.

Example:

httpClient := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    },
}
client := stromboli.NewClient(url,
    stromboli.WithHTTPClient(httpClient),
)

func WithRequestHook

func WithRequestHook(hook RequestHook) Option

WithRequestHook sets a hook that is called before each HTTP request.

Use this for observability (logging, metrics) or to modify requests before they are sent. Pass nil to clear a previously set hook.

IMPORTANT: Hooks are captured at client creation time. Setting this option AFTER calling NewClient will NOT affect API calls that use the internal generated client. To use different hooks, create a new client.

Example:

client, err := stromboli.NewClient(url,
    stromboli.WithRequestHook(func(req *http.Request) {
        log.Printf("Request: %s %s", req.Method, req.URL)
    }),
)

func WithResponseHook

func WithResponseHook(hook ResponseHook) Option

WithResponseHook sets a hook that is called after each HTTP response.

Use this for observability (logging, metrics) or to inspect response headers and status codes. See ResponseHook for important caveats about body availability. Pass nil to clear a previously set hook.

IMPORTANT: Hooks are captured at client creation time. Setting this option AFTER calling NewClient will NOT affect API calls that use the internal generated client. To use different hooks, create a new client.

Example:

client, err := stromboli.NewClient(url,
    stromboli.WithResponseHook(func(resp *http.Response) {
        log.Printf("Response: %d %s", resp.StatusCode, resp.Status)
    }),
)

func WithRetries deprecated

func WithRetries(n int) Option

WithRetries sets the maximum number of retry attempts for failed requests.

Deprecated: Retry logic is not implemented. This option logs a warning and does nothing. Consider using:

  • github.com/hashicorp/go-retryablehttp for automatic retries
  • github.com/cenkalti/backoff for custom retry logic
  • github.com/avast/retry-go for simple retry patterns

This option will be removed in v1.0.

Note: The deprecation warning is logged when NewClient is called. If you use SetLogger to configure a custom logger, call it before creating clients to see this warning in your logger.

Default: 0 (no retries).

func WithStreamTimeout

func WithStreamTimeout(d time.Duration) Option

WithStreamTimeout sets the default timeout for streaming requests.

Unlike regular requests, streams are long-running connections where data arrives incrementally. This timeout applies only if no context deadline is set when calling Client.Stream, or if the existing deadline is further away than this timeout.

IMPORTANT: This is a TOTAL DURATION timeout, not an idle/inactivity timeout. The stream will be cancelled after this duration regardless of whether data is still being received. If you need idle detection (timeout when no data arrives for a period), use Stream.EventsWithContext with periodic checks or implement a custom wrapper with read deadlines.

If not set, streaming requests have no timeout by default. This can be dangerous as a stalled server may cause the client to hang indefinitely. It's recommended to either set this option or use context.WithTimeout.

A timeout of zero or negative disables the stream timeout (not recommended).

Example:

client, err := stromboli.NewClient(url,
    stromboli.WithStreamTimeout(5*time.Minute),
)

// Now Stream will automatically timeout after 5 minutes if no context deadline is set
stream, err := client.Stream(ctx, req)

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the default timeout for all requests.

The timeout applies to the entire request lifecycle, including connection establishment, request sending, and response reading. A timeout of zero means no timeout. Negative values are treated as zero.

Default: 30 seconds.

Example:

client, err := stromboli.NewClient(url,
    stromboli.WithTimeout(5*time.Minute), // Long timeout for slow operations
)

For per-request timeouts, use context.WithTimeout instead:

ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
result, err := client.Health(ctx)

func WithToken

func WithToken(token string) Option

WithToken sets the Bearer token for authenticated requests.

Use this option when you already have a valid access token and want to create an authenticated client from the start.

Pass an empty string to clear any previously set token.

Alternatively, use Client.SetToken to set the token after client creation, or Client.GetToken to obtain a new token.

Example:

client := stromboli.NewClient(url,
    stromboli.WithToken("my-access-token"),
)

// Authenticated endpoints now work
validation, err := client.ValidateToken(ctx)

func WithUserAgent

func WithUserAgent(userAgent string) Option

WithUserAgent sets a custom User-Agent header for all requests.

The User-Agent is sent with every request and can be used for server-side analytics or debugging.

Default: "stromboli-go/{version}".

Example:

client := stromboli.NewClient(url,
    stromboli.WithUserAgent("my-app/1.0.0"),
)

type PodmanOptions

type PodmanOptions struct {
	// Memory limits container memory usage.
	// Examples: "512m", "1g", "2g"
	Memory string `json:"memory,omitempty"`

	// Timeout sets the maximum execution time.
	// Examples: "30s", "5m", "1h"
	Timeout string `json:"timeout,omitempty"`

	// Cpus limits CPU usage.
	// Examples: "0.5" (half a CPU), "2" (two CPUs)
	Cpus string `json:"cpus,omitempty"`

	// CPUShares sets relative CPU weight (default 1024).
	// Lower values = lower priority.
	CPUShares int64 `json:"cpu_shares,omitempty"`

	// Volumes mounts host paths into the container.
	// Format: "host_path:container_path" or "host_path:container_path:options"
	// Options: "ro" (read-only), "rw" (read-write, default)
	// Example: []string{"/data:/data:ro", "/workspace:/workspace"}
	Volumes []string `json:"volumes,omitempty"`

	// Image overrides the container image.
	// Must match server-configured allowed patterns.
	// Example: "python:3.12"
	Image string `json:"image,omitempty"`

	// SecretsEnv injects Podman secrets as environment variables.
	// Key: environment variable name, Value: Podman secret name.
	// The secret must exist (created via `podman secret create`).
	// Example: map[string]string{"GH_TOKEN": "github-token"}
	SecretsEnv map[string]string `json:"secrets_env,omitempty"`

	// Lifecycle configures commands to run at specific container lifecycle stages.
	// See [LifecycleHooks] for available hooks.
	Lifecycle *LifecycleHooks `json:"lifecycle,omitempty"`

	// Environment specifies a compose-based multi-service environment.
	// When set, the agent runs inside the specified service of the compose stack.
	// See [EnvironmentConfig] for configuration options.
	Environment *EnvironmentConfig `json:"environment,omitempty"`
}

PodmanOptions configures the container execution environment.

Use these options to control resource limits, mount volumes, and configure container behavior.

Example:

&stromboli.PodmanOptions{
    Memory:  "2g",
    Timeout: "10m",
    Volumes: []string{"/home/user/project:/workspace:ro"},
}

type PullImageRequest

type PullImageRequest struct {
	// Image is the image reference to pull (required).
	// Example: "python:3.12-slim"
	Image string `json:"image"`

	// Platform specifies the platform for multi-arch images.
	// Example: "linux/amd64", "linux/arm64"
	Platform string `json:"platform,omitempty"`

	// Quiet suppresses pull progress output.
	Quiet bool `json:"quiet,omitempty"`
}

PullImageRequest represents a request to pull a container image.

Use with Client.PullImage:

result, err := client.PullImage(ctx, &stromboli.PullImageRequest{
    Image:    "python:3.12-slim",
    Platform: "linux/amd64",
})

type PullImageResponse

type PullImageResponse struct {
	// Success indicates if the pull was successful.
	Success bool `json:"success,omitempty"`

	// Image is the pulled image reference.
	// Example: "python:3.12-slim"
	Image string `json:"image,omitempty"`

	// ImageID is the pulled image's ID.
	// Example: "sha256:abc123def456"
	ImageID string `json:"image_id,omitempty"`
}

PullImageResponse represents the result of an image pull operation.

type RequestHook

type RequestHook func(req *http.Request)

RequestHook is called before each HTTP request is sent. Use this for logging, metrics, or modifying requests.

type ResponseHook

type ResponseHook func(resp *http.Response)

ResponseHook is called after each HTTP response is received.

WARNING: For most API methods (Run, Health, etc.), the response body will be consumed by the generated client before your hook runs. The hook is primarily useful for inspecting headers and status codes, not body content. For the Stream method, the body is available as it hasn't been consumed yet.

Use this for logging, metrics, or inspecting response metadata.

type RunRequest

type RunRequest struct {
	// Prompt is the message to send to Claude. Required.
	Prompt string `json:"prompt"`

	// Workdir is the working directory inside the container.
	// Use Podman.Volumes to mount host paths into the container.
	// Example: "/workspace"
	Workdir string `json:"workdir,omitempty"`

	// WebhookURL is called when an async job completes.
	// Only used with [Client.RunAsync].
	// Example: "https://example.com/webhook"
	WebhookURL string `json:"webhook_url,omitempty"`

	// Claude contains Claude-specific configuration options.
	// See [ClaudeOptions] for available settings.
	Claude *ClaudeOptions `json:"claude,omitempty"`

	// Podman contains container configuration options.
	// See [PodmanOptions] for available settings.
	Podman *PodmanOptions `json:"podman,omitempty"`
}

RunRequest represents a request to execute Claude in an isolated container.

At minimum, you must provide a Prompt. All other fields are optional and provide fine-grained control over Claude's execution environment.

Basic usage:

result, err := client.Run(ctx, &stromboli.RunRequest{
    Prompt: "Hello, Claude!",
})

With options:

result, err := client.Run(ctx, &stromboli.RunRequest{
    Prompt:  "Review this code",
    Workdir: "/workspace",
    Claude: &stromboli.ClaudeOptions{
        Model:       stromboli.ModelHaiku,
        MaxBudgetUSD: 1.0,
    },
    Podman: &stromboli.PodmanOptions{
        Memory:  "1g",
        Timeout: "5m",
    },
})

type RunResponse

type RunResponse struct {
	// ID is the unique execution identifier.
	// Example: "run-abc123def456"
	ID string `json:"id"`

	// Status indicates execution result.
	// Values: "completed" (success) or "error" (failure).
	Status string `json:"status"`

	// Output contains Claude's response when Status is "completed".
	Output string `json:"output,omitempty"`

	// Error contains the error message when Status is "error".
	Error string `json:"error,omitempty"`

	// SessionID can be used to continue this conversation.
	// Pass this to RunRequest.Claude.SessionID for follow-up requests.
	SessionID string `json:"session_id,omitempty"`
}

RunResponse represents the result of a synchronous Claude execution.

Important: A nil error from Client.Run means the API call succeeded, not necessarily that Claude execution succeeded. Always check Status and Error fields to determine if the execution completed successfully.

Check Status to determine if execution succeeded:

result, err := client.Run(ctx, req)
if err != nil {
    log.Fatal(err) // API call failed
}
if result.IsSuccess() {
    fmt.Println(result.Output)
} else {
    // Execution failed - check result.Error for details
    fmt.Printf("Execution failed: %s\n", result.Error)
}

func (*RunResponse) IsSuccess

func (r *RunResponse) IsSuccess() bool

IsSuccess returns true if the execution completed successfully.

type SearchImagesOptions

type SearchImagesOptions struct {
	// Query is the search term (required).
	// Example: "python"
	Query string

	// Limit is the maximum number of results to return.
	// Default varies by registry.
	Limit int64

	// NoTrunc disables truncation of output.
	NoTrunc bool
}

SearchImagesOptions configures an image search request.

Example:

results, err := client.SearchImages(ctx, &stromboli.SearchImagesOptions{
    Query:   "python",
    Limit:   25,
    NoTrunc: true,
})

type Secret

type Secret struct {
	// ID is the unique identifier of the secret.
	// Example: "abc123def456"
	ID string `json:"id,omitempty"`

	// Name is the secret name used to reference it.
	// Example: "github-token"
	Name string `json:"name"`

	// CreatedAt is when the secret was created (RFC3339 format).
	// Example: "2024-01-15T10:30:00Z"
	CreatedAt string `json:"created_at,omitempty"`
}

Secret represents a Podman secret's metadata.

Secrets are used to securely pass sensitive data (API keys, tokens, etc.) to containers without exposing them in environment variables or command lines.

Use Client.CreateSecret to create a new secret:

err := client.CreateSecret(ctx, &stromboli.CreateSecretRequest{
    Name:  "github-token",
    Value: "ghp_xxxx...",
})

func (*Secret) CreatedAtTime

func (s *Secret) CreatedAtTime() time.Time

CreatedAtTime parses CreatedAt as time.Time. Returns zero time if CreatedAt is empty or parsing fails.

NOTE: Parsing errors are silently ignored. If you need to validate the timestamp format, use time.Parse(time.RFC3339, s.CreatedAt) directly.

type Stream

type Stream struct {
	// contains filtered or unexported fields
}

Stream represents an active SSE stream from Claude.

Use Client.Stream to create a stream, then iterate over events:

stream, err := client.Stream(ctx, &stromboli.StreamRequest{
    Prompt: "Count from 1 to 10",
})
if err != nil {
    log.Fatal(err)
}
defer stream.Close()

for stream.Next() {
    event := stream.Event()
    fmt.Print(event.Data)
}

if err := stream.Err(); err != nil {
    log.Fatal(err)
}

func (*Stream) Close

func (s *Stream) Close() error

Close closes the stream and releases resources.

Always call Close when done with the stream, preferably with defer. This is required even if Stream.Next returns false due to an error, as the underlying HTTP response body must be closed to release resources.

Close is safe to call multiple times and is thread-safe.

Example:

stream, err := client.Stream(ctx, req)
if err != nil {
    log.Fatal(err)
}
defer stream.Close() // Always close, even on errors

func (*Stream) Err

func (s *Stream) Err() error

Err returns any error that occurred during streaming.

Returns nil if:

  • The stream completed successfully (normal EOF)
  • The stream is still active
  • No error has occurred yet

To distinguish between "completed successfully" and "still active", check if Stream.Next returned false. After Next returns false, Err() == nil means normal completion; Err() != nil means an error.

func (*Stream) Event

func (s *Stream) Event() *StreamEvent

Event returns the current event.

Call this after Stream.Next returns true. If called before the first successful Stream.Next call, returns an empty event (not nil) to prevent nil pointer dereferences.

This method is thread-safe and can be called concurrently with Stream.Next.

func (*Stream) Events deprecated

func (s *Stream) Events() <-chan *StreamEvent

Events returns a channel that yields events from the stream.

The channel is closed when the stream ends or an error occurs. Check Stream.Err after the channel closes to see if an error occurred.

Deprecated: Use Stream.EventsWithContext to avoid goroutine leaks if you stop reading before the stream ends.

Example:

for event := range stream.Events() {
    fmt.Print(event.Data)
}
if err := stream.Err(); err != nil {
    log.Fatal(err)
}

func (*Stream) EventsWithContext

func (s *Stream) EventsWithContext(ctx context.Context) <-chan *StreamEvent

EventsWithContext returns a channel that yields events from the stream.

The channel is closed when the stream ends, an error occurs, or the context is cancelled. This is the preferred method to avoid goroutine leaks if you stop reading before the stream ends.

Example:

ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

for event := range stream.EventsWithContext(ctx) {
    fmt.Print(event.Data)
}
if err := stream.Err(); err != nil {
    log.Fatal(err)
}

func (*Stream) Next

func (s *Stream) Next() bool

Next advances to the next event in the stream.

Returns true if an event is available, false if the stream is exhausted or an error occurred. Call Stream.Err to check for errors.

Example:

for stream.Next() {
    event := stream.Event()
    fmt.Print(event.Data)
}

type StreamEvent

type StreamEvent struct {
	// Type is the event type (from "event:" line).
	// Common types: "", "message", "error", "done"
	Type string

	// Data is the event payload (from "data:" line).
	Data string

	// ID is the event ID (from "id:" line), if provided.
	ID string
}

StreamEvent represents a single event from the SSE stream.

SSE events have an optional event type and data payload. Most events will have Type empty and Data containing the output.

type StreamRequest

type StreamRequest struct {
	// Prompt is the message to send to Claude. Required.
	Prompt string

	// Workdir is the working directory inside the container.
	Workdir string

	// SessionID enables conversation continuation.
	SessionID string
}

StreamRequest represents a request for streaming Claude output.

This is a simplified version of RunRequest for the streaming endpoint, which only supports a subset of options via query parameters.

type TokenResponse

type TokenResponse struct {
	// AccessToken is the JWT access token for API authentication.
	// Use with [WithToken] option or pass to authenticated endpoints.
	AccessToken string `json:"access_token"`

	// RefreshToken is used to obtain new access tokens.
	// Use with [Client.RefreshToken] when the access token expires.
	RefreshToken string `json:"refresh_token"`

	// ExpiresIn is the access token lifetime in seconds.
	// Example: 3600 (1 hour)
	ExpiresIn int64 `json:"expires_in"`

	// TokenType is the token type, typically "Bearer".
	TokenType string `json:"token_type"`
}

TokenResponse represents JWT tokens returned by authentication endpoints.

Use Client.GetToken to obtain tokens:

tokens, err := client.GetToken(ctx, "my-client-id")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Access token: %s\n", tokens.AccessToken)

type TokenValidation

type TokenValidation struct {
	// Valid indicates whether the token is valid.
	Valid bool `json:"valid"`

	// Subject is the token subject (typically client ID).
	Subject string `json:"subject"`

	// ExpiresAt is the token expiration time as Unix timestamp.
	ExpiresAt int64 `json:"expires_at"`
}

TokenValidation represents the result of validating a JWT token.

Use Client.ValidateToken to validate the current token:

validation, err := client.ValidateToken(ctx)
if err != nil {
    log.Fatal(err)
}
if validation.Valid {
    fmt.Printf("Token valid until: %d\n", validation.ExpiresAt)
}

Directories

Path Synopsis
generated

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL