Skip to main content

What this doc covers

  • How to build a “chat with repo” feature using Factory’s Droid Exec in headless mode
  • Setting up streaming responses with Server-Sent Events for real-time agent feedback
  • Understanding the actual implementation from Factory’s official example
For a complete reference on Droid Exec capabilities, see the Droid Exec Overview

1. Pre-requisites and Installation

Requirements

  • Bun (the example uses Bun, but Node.js works too)
  • Factory CLI installed (droid on your PATH)
  • A local repository to chat with

Installation

# Install Bun
curl -fsSL https://bun.sh/install | bash

# Install Factory CLI
curl -fsSL https://app.factory.ai/cli | sh

# Sign in to Factory (one-time browser auth)
droid
After the browser login, droid exec works from your app without needing API keys in code.

Try the Official Example

git clone https://github.com/Factory-AI/examples.git
cd examples/droid-chat
bun i
bun dev
Open http://localhost:4000 - you’ll see a chat window overlaid on the repo’s README.

2. Why Use Droid Exec?

Building AI features that understand codebases requires orchestrating multiple operations: searching files, reading code, analyzing structure, and synthesizing answers. Without droid exec, a question like “How do we charge for MCP servers?” would require dozens of API calls and custom logic to search, read, and understand the relevant code. droid exec is Factory’s headless CLI mode that handles this autonomously in a single command. It searches codebases, reads files, reasons about code structure, and returns structured JSON output—with built-in safety controls and configurable autonomy levels. Perfect for building chat interfaces, CI/CD automation, or any application that needs codebase intelligence.

3. How It Works: The Core Pattern

The Factory example uses a simple pattern: spawn droid exec with --output-format debug and stream the results via Server-Sent Events (SSE).

Running Droid Exec

// Simplified from src/server/chat.ts
function runDroidExec(prompt: string, repoPath: string) {
  const args = ["exec", "--output-format", "debug"];
  
  // Optional: configure model (defaults to glm-4.6)
  const model = process.env.DROID_MODEL_ID ?? "glm-4.6";
  args.push("-m", model);
  
  // Optional: reasoning level (off|low|medium|high)
  const reasoning = process.env.DROID_REASONING;
  if (reasoning) {
    args.push("-r", reasoning);
  }
  
  args.push(prompt);
  
  return Bun.spawn(["droid", ...args], {
    cwd: repoPath,
    stdio: ["ignore", "pipe", "pipe"]
  });
}

Key Flags Explained

--output-format debug: Streams structured events as the agent works
  • Each tool call (file read, search, etc.) emits an event
  • Lets you show real-time progress to users
  • Alternative: --output-format json for final output only
-m (model): Choose your AI model
  • glm-4.6 - Fast, cheap (default)
  • gpt-5-codex - Most powerful for complex code
  • claude-sonnet-4-5 - Best balance of speed and capability
-r (reasoning): Control thinking depth
  • off - No reasoning, fastest
  • low - Light reasoning (default)
  • medium|high - Deeper analysis, slower
No --auto flag?: Defaults to read-only (safest)
  • Can’t modify files, only read/search/analyze
  • Perfect for chat applications
See CLI Reference for all flag explanations

4. Building the Chat Feature: Streaming with SSE

The Factory example streams agent activity in real-time using Server-Sent Events. This gives users immediate feedback as the agent searches, reads files, and thinks.

Server: Streaming SSE Endpoint

// Simplified from src/server/chat.ts
export async function handleChatRequest(req: Request): Promise<Response> {
  const { message, history } = await req.json();
  
  // Get repo info (finds ./repos/<folder>)
  const repoInfo = await getLocalRepoInfo();
  
  // Build prompt with history
  const prompt = buildPrompt(message, history);
  
  // Spawn droid exec
  const proc = runDroidExec(prompt, repoInfo.workdir);
  
  // Create SSE stream
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder();
      
      // Helper to send events
      const send = (event: string, data: any) => {
        controller.enqueue(encoder.encode(`event: ${event}\n`));
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
      };
      
      // Read stdout and parse debug events
      const reader = proc.stdout.getReader();
      let buffer = "";
      
      (async () => {
        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          
          buffer += new TextDecoder().decode(value);
          buffer = parseAndFlush(buffer, (event, data) => {
            send(event, data);
          });
        }
      })();
      
      // When process exits, close stream
      proc.exited.then((code) => {
        send("exit", { code });
        controller.close();
      });
    }
  });
  
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive"
    }
  });
}

Event Types You’ll Receive

When --output-format debug is used, droid emits events like:
event: tool_call
data: {"tool":"grep","args":{"pattern":"MCP"}}

event: assistant_chunk
data: {"text":"I found references to MCP servers in..."}

event: tool_result
data: {"files_found":["src/billing.ts","config/pricing.yml"]}

event: exit
data: {"code":0}

Client: React Hook for SSE

// Simplified from src/hooks/useChat.ts
export function useChat() {
  const [messages, setMessages] = useState<Message[]>([]);
  
  const sendMessage = async (text: string, history: Message[]) => {
    // Add user message
    setMessages(prev => [...prev, { role: "user", content: text }]);
    
    // Start SSE connection
    const response = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: text, history })
    });
    
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let assistantMessage = { role: "assistant", content: "" };
    
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      
      buffer += decoder.decode(value, { stream: true });
      
      // Parse SSE events
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";
      
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        
        if (line.startsWith("event:")) {
          const event = line.slice(7);
          const dataLine = lines[++i];
          const data = JSON.parse(dataLine.slice(6));
          
          if (event === "assistant_chunk") {
            // Append to assistant message
            assistantMessage.content += data.text;
            setMessages(prev => {
              const newMessages = [...prev];
              if (newMessages[newMessages.length - 1]?.role !== "assistant") {
                newMessages.push({ ...assistantMessage });
              } else {
                newMessages[newMessages.length - 1] = { ...assistantMessage };
              }
              return newMessages;
            });
          }
          
          if (event === "exit") {
            // Done
            break;
          }
        }
      }
    }
  };
  
  return { messages, sendMessage };
}

Real-World Example: The Video

In the demo video, the user asked: “Can you search for how we charge for MCP servers?” Behind the scenes, droid exec automatically:
  1. Searched the codebase with ripgrep for “MCP”, “charge”, “payment”
  2. Read relevant files (billing config, pricing logic, env vars)
  3. Analyzed the code structure to understand the charging flow
  4. Synthesized a complete answer with file locations, variable names, and implementation details
All streamed in real-time through SSE - no manual orchestration needed.

Project Structure (from the Example)

examples/droid-chat/
├── src/
│   ├── server/
│   │   ├── index.ts       # Bun HTTP server + static files
│   │   ├── chat.ts        # SSE endpoint, runs droid exec
│   │   ├── repo.ts        # Finds local repo in ./repos/
│   │   ├── prompt.ts      # System prompt + history formatting
│   │   └── stream.ts      # Parses debug output, strips paths
│   ├── components/chat/   # React chat UI
│   └── hooks/useChat.ts   # Client-side SSE parsing
├── repos/                 # Your repositories to chat with
│   └── your-repo/
└── public/                # Static assets

Configuration Options

The example supports environment variables:
# .env
DROID_MODEL_ID=gpt-5-codex  # Default: glm-4.6
DROID_REASONING=low         # Default: low (off|low|medium|high)
PORT=4000                   # Default: 4000
HOST=localhost              # Default: localhost

Best Practices

Do:
  • Use read-only mode (no --auto flag) for user-facing features
  • Validate user input before passing to droid exec
  • Set timeouts (example uses 240 seconds)
  • Parse SSE events incrementally for responsive UI
  • Strip local file paths from debug output before sending to client
⚠️ Avoid:
  • Using --auto medium/high in production without sandboxing
  • Passing unsanitized user input directly to the CLI
  • Blocking the main thread while waiting for results

5. Customization & Extensions

Swap the Data Source

The example ships with a local repo, but you can easily adapt it: PDFs & Documents:
// Extract text from PDFs, write to temp dir, point droid at it
import { pdfToText } from 'pdf-to-text';

const text = await pdfToText('document.pdf');
fs.writeFileSync('/tmp/docs/content.txt', text);
runDroidExec("Summarize this document", '/tmp/docs');
Databases:
// Add database context to prompt
const prompt = `You have access to a PostgreSQL database with these tables:
${JSON.stringify(schema)}

User question: ${message}`;

runDroidExec(prompt, repoPath); // Can read SQL files in repo
Websites:
// Crawl site, save markdown, chat with it
import TurndownService from 'turndown';

const markdown = new TurndownService().turndown(html);
fs.writeFileSync('./repos/site-content/page.md', markdown);

Change Models on the Fly

// Let users pick models
function runWithModel(prompt: string, model: string) {
  return Bun.spawn([
    "droid", "exec",
    "-m", model,  // glm-4.6, gpt-5-codex, etc.
    "--output-format", "debug",
    prompt
  ], { cwd: repoPath });
}

Add Tool Call Visibility

The example’s stream.ts parses debug events. You can surface them in the UI:
if (event === "tool_call") {
  // Show: "🔍 Searching for 'MCP charge'"
  // Show: "📄 Reading src/billing.ts"
}
This creates a transparent, trust-building experience where users see exactly what the agent is doing.

Additional Resources

Official Example: Documentation: Community:

Next Steps

  1. Clone the example: git clone https://github.com/Factory-AI/examples.git
  2. Run it locally: cd examples/droid-chat && bun dev
  3. Explore the code in src/server/chat.ts to see how SSE streaming works
  4. Customize src/server/prompt.ts to change the agent’s behavior
  5. Swap ./repos/ content to chat with your own repositories
The example is intentionally minimal (~500 lines total) so you can understand it fully and adapt it to your needs.