Skip to content

Client Examples

Copy-pasteable examples for calling Stromboli from different languages and common automation scenarios.

curl

# Simple prompt
curl -X POST localhost:8080/run \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Hello, Claude!"}'

# With workspace, auth, and options
curl -X POST localhost:8080/run \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "prompt": "Analyze the code structure",
    "workdir": "/workspace",
    "podman": {"volumes": ["/home/user/myproject:/workspace"]},
    "claude": {"model": "sonnet", "max_budget_usd": 1.00}
  }'

# Async with webhook
curl -X POST localhost:8080/run/async \
  -d '{
    "prompt": "Refactor this codebase",
    "webhook_url": "https://myserver.com/webhook"
  }'

# Stream output
curl -N "localhost:8080/run/stream?prompt=Hello"

Python

import requests
import json

class StromboliClient:
    def __init__(self, base_url="http://localhost:8080", token=None):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        if token:
            self.session.headers["Authorization"] = f"Bearer {token}"

    def run(self, prompt, workdir=None, volumes=None, model="sonnet", **kwargs):
        payload = {"prompt": prompt, "claude": {"model": model}}
        if workdir:
            payload["workdir"] = workdir
        if volumes:
            payload.setdefault("podman", {})["volumes"] = volumes
        for key in ("max_budget_usd", "allowed_tools"):
            if key in kwargs:
                payload["claude"][key] = kwargs[key]
        resp = self.session.post(f"{self.base_url}/run", json=payload)
        resp.raise_for_status()
        return resp.json()

    def run_async(self, prompt, webhook_url=None, **kwargs):
        payload = {"prompt": prompt}
        if webhook_url:
            payload["webhook_url"] = webhook_url
        resp = self.session.post(f"{self.base_url}/run/async", json=payload)
        resp.raise_for_status()
        return resp.json()

    def get_job(self, job_id):
        resp = self.session.get(f"{self.base_url}/jobs/{job_id}")
        resp.raise_for_status()
        return resp.json()

    def stream(self, prompt, workdir=None):
        params = {"prompt": prompt}
        if workdir:
            params["workdir"] = workdir
        resp = self.session.get(f"{self.base_url}/run/stream", params=params, stream=True)
        resp.raise_for_status()
        for line in resp.iter_lines(decode_unicode=True):
            if line and line.startswith("data: "):
                yield json.loads(line[6:])

# Usage
client = StromboliClient()
result = client.run(
    "Analyze this project",
    workdir="/workspace",
    volumes=["/home/user/myproject:/workspace"],
)
print(result["output"])

JavaScript

class StromboliClient {
  constructor(baseUrl = 'http://localhost:8080', token = null) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.token = token;
  }

  async #fetch(path, options = {}) {
    const headers = { 'Content-Type': 'application/json', ...options.headers };
    if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
    const response = await fetch(`${this.baseUrl}${path}`, { ...options, headers });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response;
  }

  async run(prompt, { workdir, model = 'sonnet', volumes, maxBudget } = {}) {
    const payload = { prompt, claude: { model } };
    if (workdir) payload.workdir = workdir;
    if (volumes) payload.podman = { volumes };
    if (maxBudget) payload.claude.max_budget_usd = maxBudget;
    const resp = await this.#fetch('/run', { method: 'POST', body: JSON.stringify(payload) });
    return resp.json();
  }

  async *stream(prompt, { workdir } = {}) {
    const params = new URLSearchParams({ prompt });
    if (workdir) params.set('workdir', workdir);
    const resp = await this.#fetch(`/run/stream?${params}`);
    const reader = resp.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';
      for (const line of lines) {
        if (line.startsWith('data: ')) yield JSON.parse(line.slice(6));
      }
    }
  }
}

// Usage
const client = new StromboliClient();
const result = await client.run('Analyze this project', {
  workdir: '/workspace',
  volumes: ['/home/user/myproject:/workspace'],
});
console.log(result.output);

Go

package main

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

type Client struct {
    BaseURL string
    Token   string
}

type RunRequest struct {
    Prompt  string          `json:"prompt"`
    Workdir string          `json:"workdir,omitempty"`
    Claude  *ClaudeOptions  `json:"claude,omitempty"`
    Podman  *PodmanOptions  `json:"podman,omitempty"`
}

type ClaudeOptions struct {
    Model        string   `json:"model,omitempty"`
    MaxBudgetUSD *float64 `json:"max_budget_usd,omitempty"`
    MaxTurns     *int     `json:"max_turns,omitempty"`
}

type PodmanOptions struct {
    Volumes []string `json:"volumes,omitempty"`
}

type RunResponse struct {
    Output           string          `json:"output"`
    StructuredOutput json.RawMessage `json:"structured_output,omitempty"`
    SessionID        string          `json:"session_id"`
}

func (c *Client) Run(ctx context.Context, req *RunRequest) (*RunResponse, error) {
    body, _ := json.Marshal(req)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/run", bytes.NewReader(body))
    httpReq.Header.Set("Content-Type", "application/json")
    if c.Token != "" {
        httpReq.Header.Set("Authorization", "Bearer "+c.Token)
    }
    resp, err := http.DefaultClient.Do(httpReq)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        b, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, b)
    }
    var result RunResponse
    json.NewDecoder(resp.Body).Decode(&result)
    return &result, nil
}

// Usage:
// client := &Client{BaseURL: "http://localhost:8080"}
// result, _ := client.Run(context.Background(), &RunRequest{
//     Prompt:  "Analyze this code",
//     Workdir: "/workspace",
//     Podman:  &PodmanOptions{Volumes: []string{"/home/user/project:/workspace"}},
// })
//
// // When using json_schema, check structured_output:
// if result.StructuredOutput != nil {
//     var data MyStruct
//     json.Unmarshal(result.StructuredOutput, &data)
// }

CI/CD code review

Run Stromboli as a service container in GitHub Actions to review PRs:

# .github/workflows/ai-review.yml
name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    services:
      stromboli:
        image: ghcr.io/tomblancdev/stromboli:latest
        ports:
          - 8080:8080
        volumes:
          - /home/runner/work:/workspace
        env:
          STROMBOLI_AUTH_ENABLED: "false"

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup credentials
        run: echo '${{ secrets.CLAUDE_CREDENTIALS }}' > .claude-credentials.json

      - name: Wait for Stromboli
        run: |
          for i in {1..30}; do curl -s localhost:8080/health && break; sleep 1; done

      - name: Review
        run: |
          FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | tr '\n' ' ')
          REVIEW=$(curl -s -X POST localhost:8080/run \
            -H "Content-Type: application/json" \
            -d "{
              \"prompt\": \"Review these changed files for bugs and improvements: $FILES\",
              \"workdir\": \"/workspace/${{ github.repository }}/${{ github.ref_name }}\",
              \"claude\": {\"model\": \"sonnet\", \"allowed_tools\": [\"Read\", \"Grep\", \"Glob\"]}
            }" | jq -r '.output')
          echo "$REVIEW" > review.md

      - name: Post comment
        uses: actions/github-script@v7
        with:
          script: |
            const review = require('fs').readFileSync('review.md', 'utf8');
            github.rest.issues.createComment({
              owner: context.repo.owner, repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## AI Code Review\n\n${review}`
            });

Batch code migration

Apply a migration across multiple files using Python:

import json

client = StromboliClient(token="your-token")

# Find files that need migration
result = client.run(
    "Find all files using print() instead of logging. Output as JSON: {\"files\": [...]}",
    workdir="/workspace",
    volumes=["/home/user/myproject:/workspace"],
    allowed_tools=["Glob", "Grep", "Read"],
)
files = json.loads(result["output"])["files"]

# Migrate each file
for f in files:
    client.run(
        f"Replace print() with logging in {f}. Import logging if needed.",
        workdir="/workspace",
        volumes=["/home/user/myproject:/workspace"],
        allowed_tools=["Read", "Edit"],
    )

Test generator

Generate tests for existing code:

client = StromboliClient(token="your-token")
result = client.run(
    "Analyze src/utils.py and generate pytest tests covering all public functions. "
    "Include edge cases. Write the tests to tests/test_utils.py.",
    workdir="/workspace",
    volumes=["/home/user/myproject:/workspace"],
    allowed_tools=["Read", "Glob", "Grep", "Write"],
)