@factory/cli
v0.55.2
Published
Factory Droid CLI - AI-powered software engineering agent
Downloads
167
Readme
Factory CLI - Demo Edit
A hybrid command-line interface that runs either:
- Interactive TUI – a full-screen React/Ink terminal app
- Headless commands – traditional
droid headless <command>sub-commands powered by Commander
The entry-point (src/index.ts) detects how it was invoked and chooses the right mode automatically.
1. Overview of the Hybrid Architecture
src/
├── index.ts # Hybrid entry – mode detection
│
├── app.tsx # React/Ink TUI (interactive mode)
│
└── commands/ # Commander commands (headless mode)
├── droid.ts
└── login.ts• No positional args ➜ Interactive TUI
• headless subcommand ➜ Headless mode
2 · Local Development
All dev tasks are exposed as npm scripts – never run compiled .js files directly.
| Purpose | Command |
| -------------------------------- | --------------------- |
| Start CLI (auto mode) | npm start |
| Start with Node inspector | npm run debug |
| Set up shell alias for local dev | npm run setup:alias |
| Build SEA for current platform | npm run build |
| Build and run the binary | npm run build:run |
| Lint source | npm run lint |
| Type-check | npm run typecheck |
| Run tests | npm test |
| Produce executable bundle | npm run bundle |
| Clean build artifacts | npm run clean |
The start/debug scripts use tsx so you can edit TypeScript and restart instantly.
Shell Alias Setup
For easier local development, you can set up a droid alias that runs the CLI from your current directory:
# Add alias to ~/.zshrc (checks if already exists)
npm run setup:alias
# Start a new terminal or source the file
source ~/.zshrc
# Now you can run 'droid' from anywhere
droidBuilding Standalone Executables (SEA)
The CLI can be compiled into Single Executable Applications (SEA) for different platforms:
# Build for your current platform automatically
npm run build
# Build and immediately run the binary
npm run build:run
# Build for specific platforms
npm run build:sea:mac:arm64 # macOS Apple Silicon
npm run build:sea:mac:x64 # macOS Intel
npm run build:sea:linux:arm64 # Linux ARM64
npm run build:sea:linux:x64 # Linux x64
npm run build:sea:windows:x64 # Windows x64The build script automatically detects your OS and architecture, then builds the appropriate binary to dist/{platform}/{arch}/droid (or droid.exe on Windows).
3 · Testing Both Modes Locally
Interactive TUI
# Launch interactive UI
npm startYou'll see a colourful Ink interface; quit with Ctrl-C.
Running in VSCode
Factory CLI can also be run inside VSCode using the Factory extension:
- First, install the Factory VSCode extension (see VSCode Extension README for installation instructions)
- Click the Run Factory button (🤖) in the editor toolbar to launch Factory CLI in a dedicated terminal
- The extension provides full VSCode context (open files, selections, diagnostics) to Factory via MCP
Headless Commands
# Show global help
npm start -- --help
# Show headless subcommands
npm start -- headless --help
# Run login interactively (headless)
npm start -- headless login
# Send message to a droid
npm start -- headless droid "Hello, Droid!" --session-id <sessionId>The extra -- after npm start passes subsequent flags to the CLI.
4 · Development vs Production
| Phase | Command(s) | Result |
| ----------- | ------------------------------------ | ---------------------------------------------- |
| Dev | npm start / npm run debug | Runs from TS sources with tsx, fast reload. |
| Bundle | npm run bundle (calls build) | Generates single executable bundle/droid.js. |
| Publish | npm publish (bundled in prepare) | Users install droid binary from npm. |
During CI the prepare script produces the bundle automatically.
5 · Examples
Headless examples
# Show authentication status
droid headless status
# Authenticate (opens browser)
droid headless login
# Talk to Droid
droid headless "Hello" --session-id dOLpXUI8ux6YdZrg3kCs
# Streaming input mode
# Send multiple messages over stdin, maintaining conversation state
echo '{"role":"user","content":"hello"}' | droid exec --input-format stream-json --output-format stream-jsonStreaming Input Mode (Droid Manager Integration)
The CLI supports a streaming input mode designed for Droid Manager integration, allowing a single process to handle multiple user messages while maintaining conversation state.
Usage Pattern:
// Spawn process once
const droid = spawn('droid', [
'exec',
'--input-format',
'stream-json',
'--output-format',
'stream-json',
'--auto',
'low',
]);
// Send messages over time (JSONL format via stdin)
droid.stdin.write('{"role":"user","content":"hello"}\n');
// Read events from stdout (JSONL format)
droid.stdout.on('data', (chunk) => {
const events = chunk.toString().split('\n').filter(Boolean);
events.forEach((line) => {
const event = JSON.parse(line);
// Handle: user messages, assistant responses, tool calls, etc.
});
});
// Send another message later
droid.stdin.write('{"role":"user","content":"what can you do?"}\n');
// Close stdin when done to trigger graceful shutdown
droid.stdin.end();Key Points:
- Input: JSONL (newline-delimited JSON) via stdin, each line is
Anthropic.MessageParamwithrole: "user" - Output: JSONL events via stdout (same format as
--output-format stream-json) - stdin stays open until explicitly closed by the caller
- Messages processed sequentially - one at a time
- Session persists across all messages in the process lifetime
- Graceful shutdown when stdin closes
Interactive example
# Simply run with no args
droid6 · Testing the Production droid Command
Sometimes you need to test the exact binary users will get from npm
install -g factory-cli.
Follow this workflow:
# 1. Build optimised bundle (also compiles TS → JS)
npm run bundle
# 2. Link globally so `droid` is on your PATH
npm link
# 3. Use it anywhere
droid --help
droid headless status
droid headless "Hello" --session-id <sessionId>
# 4. (Optional) Un-link when finished
npm unlink -g factory-cli| Situation | Command to use |
| --------------------------- | --------------------------------------------------------- |
| Fast iteration / TypeScript | npm start -- <args> |
| Debug with inspector | npm run debug -- <args> |
| Validate production bundle | npm run bundle && npm link then droid headless <args> |
ℹ️ Tip: The extra -- after npm start or npm run debug passes the
remaining flags directly to the CLI.
7 · ESM & Imports
The package is "type": "module"; all runtime imports use .js extensions even though the source is TypeScript. The build pipeline rewrites them automatically.
8 · Troubleshooting
| Problem | Fix |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| EACCES when running droid | Ensure the bundle is executable (chmod +x bundle/droid.js). npm run bundle handles this automatically. |
| module not found after rename | Run npm run clean && npm run bundle to rebuild from scratch. |
| Global command still points to old code | Run npm unlink -g factory-cli && npm link to refresh the symlink. |
9 · Logging
Factory CLI has two different logging behaviors depending on the execution mode:
Interactive Mode Logging
When running in interactive TUI mode (droid with no arguments), all logging output is redirected to files to avoid interfering with the clean React/Ink interface:
- Log Directory:
~/.factory/logs/ - Log Files:
droid-log-<timestamp>.log(e.g.,droid-log-2025-01-15T10-30-45-123Z.log) - Content: All
logInfo,logException, andlogWarncalls are written to the timestamped log file - Format:
[timestamp] LEVEL: message | Context: {...}
Example log file location:
~/.factory/logs/droid-log-2025-01-15T10-30-45-123Z.logExample log entry:
[2025-01-15T10:30:45.123Z] INFO: User started interactive session
[2025-01-15T10:30:47.456Z] ERROR: Failed to initialize MCP client: Connection refused | Context: {"retry": 1}Headless Mode Logging
When running headless commands (droid headless <command>), logging follows standard console output patterns:
- Log Output: Directly to stdout/stderr using the standard
@factory/loggingpackage - Integration: Works with existing telemetry, Sentry, and monitoring systems
- Format: Standard Factory logging format with metadata support
Accessing Logs
Interactive Mode Logs:
# View the most recent log file
ls -la ~/.factory/logs/
# Tail the logs in real-time (find the most recent file)
tail -f ~/.factory/logs/droid-log-*.log
# View logs from a specific session
cat ~/.factory/logs/droid-log-2025-01-15T10-30-45-123Z.logHeadless Mode Logs:
# Logs appear directly in terminal output
droid headless "test message" --session-id abc123
# Redirect to file if needed
droid headless "test" --session-id abc123 2>&1 | tee my-session.logLog Cleanup
Interactive mode creates a new log file for each session. To manage disk space:
# Remove logs older than 7 days
find ~/.factory/logs -name "droid-log-*.log" -mtime +7 -delete
# View total log directory size
du -sh ~/.factory/logs10 · Tool Registry & Executors Design
🔄 What Changed
After: Dynamic mapping automatically discovers tools from TUI registry
// Tools are automatically discovered from TUI registry
const toolMapping = buildToolMapping();🛠 How to Add New Tools
- Create executor in
src/tools/executors/client/(implementClientToolExecutor) - Register in TUI registry in
src/tools/tui.ts:getTUIToolRegistry().register({ tool: myNewCliTool, // from @factory/droid-core/tools/definitions executorFactory: () => new MyNewExecutor(), }); - That's it! Tool is automatically available in
executeTool()using itsllmId
11 · E2E Testing of the TUI
The Factory CLI includes end-to-end tests for the terminal UI using Microsoft's @microsoft/tui-test.
Testing Framework
- Framework:
@microsoft/tui-test - Shell: bash
- Config:
apps/cli/tui-test.config.ts
Prerequisites
The E2E tests require a valid FACTORY_API_KEY environment variable to authenticate with Factory services. You can generate an API key from https://dev.app.factory.ai/settings/api-keys.
export FACTORY_API_KEY="your-api-key-here"
npm run test:e2eThe tests launch the CLI via a wrapper script that configures the environment:
- program:
./e2e-tests/test-workspace/run-droid.sh - The wrapper exports all required env vars (e.g.,
FACTORY_HOME_OVERRIDE) and ensures correct execution context for tests. - rows/columns: 30x80
Test Layout
apps/cli/
├── e2e-tests/
│ ├── chat-input.test.ts # Text entry, deletion, Ctrl+C, file suggestions, cursor nav
│ ├── settings.template.json # Copied to each test's .factory-dev
│ ├── templates/ # Blueprint copied to each test workspace
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ └── test-workspace/ # Contains wrapper scripts only
│ ├── run-droid.sh # Creates unique workspace per invocation
│ ├── run-droid.ps1 # Windows version
│ └── run-droid.cmd # Windows CMD wrapper
└── tui-test.config.ts # TUI test config using the wrapperRunning Tests
# From apps/cli
npm run test:e2e # bundles CLI then runs all e2e tests
# Run a specific test file (or append :line)
npm run test:e2e e2e-tests/chat-input.test.ts
# Tests automatically create isolated workspaces - no setup needed!
# Each test invocation gets its own /tmp/droid-test-* directoryDebugging
At any point of time, you can access the serialized terminal view using:
const terminalView = terminal.serialize().view;Tracing is enabled in config (trace: true). Traces are saved under tui-traces/.
# Replay a trace
npm run show-trace tui-traces/<trace-file>
# Or directly
npx @microsoft/tui-test show-trace tui-traces/<trace-file>Test Environment Isolation
Each test case runs in complete isolation with its own workspace and .factory-dev directory:
- Per-test workspaces: The wrapper scripts (
run-droid.sh,run-droid.ps1) automatically create a unique temporary workspace for each test invocation - Unique identifiers: Workspaces are named with PID and timestamp:
/tmp/droid-test-{pid}-{timestamp}-XXXXXX - Template copying: Files from
e2e-tests/templates/(package.json, tsconfig.json, src/) are copied to each test workspace - Automatic cleanup: Temporary workspaces are cleaned up when tests complete (via trap on Unix, try/finally on Windows)
- Parallel execution: Tests can run in parallel without state interference since each has its own
.factory-dev
Settings template (copied to each test's .factory-dev/settings.json):
{
"model": "claude-sonnet-4-20250514",
"cloudSessionSync": false,
"diffMode": "github"
}What’s Covered
Current suite validates:
- Text entry visibility
- Backspace/Delete behavior (including forward delete)
- Ctrl+C warning and double Ctrl+C exit
- File suggestions via @: show, filter, navigate, select, dismiss
- Cursor navigation and in-line editing
Writing New Tests
Global config already provides the program and env. You can optionally tweak terminal size per test file:
import { test, expect } from '@microsoft/tui-test';
test.use({ rows: 24, columns: 100 });
test('example', async ({ terminal }) => {
await expect(
terminal.getByText('standing in an open terminal', {
full: false,
strict: false,
})
).toBeVisible({ timeout: 10000 });
terminal.write('Hello');
await expect(terminal.getByText('Hello')).toBeVisible();
});12 · Contributing
pnpm install(ornpm install) at repo rootcd apps/cli- Implement feature / fix
- Ensure
npm run lint && npm run typecheck && npm testpass - Run E2E tests with
npm run test:e2e - Commit & open PR 🚀
