| name | create-agent-tui |
| description | Scaffolds a complete agent TUI in TypeScript using @openrouter/agent — like create-react-app for terminal agents. Generates a customizable terminal interface with three input styles, four tool display modes, ASCII banners, streaming output, session persistence, and configurable tools. Use when building an agent, creating a TUI, scaffolding an agent project, or building a coding assistant. |
Create Agent TUI
Scaffolds a complete agent TUI in TypeScript targeting OpenRouter. The generated project uses @openrouter/agent for the inner loop (model calls, tool execution, stop conditions) and provides the outer shell: a customizable terminal interface, configuration, session management, tool definitions, and an entry point.
Architecture draws from three production agent systems:
- pi-mono/coding-agent — three-layer separation, JSONL sessions, pluggable tool operations
- Claude Code — tool metadata (read-only, destructive, approval), system prompt composition
- Codex CLI — layered config, approval flow with session caching, structured logging
Prerequisites
Decision Tree
Interactive Tool Checklist
Present this as a multi-select checklist. Items marked ON are pre-selected defaults.
OpenRouter Server Tools (server-side, zero implementation)
| Tool | Type string | Default | Config |
|---|
| Web Search | openrouter:web_search | ON | engine, max_results, domain filtering |
| Datetime | openrouter:datetime | ON | timezone |
| Image Generation | openrouter:image_generation | OFF | model, quality, size, format |
Server tools go in the tools array alongside user-defined tools. No client code needed — OpenRouter executes them.
User-Defined Tools (client-side, generated into src/tools/)
| Tool | Default | Description |
|---|
| File Read | ON | Read files with offset/limit, detect images |
| File Write | ON | Write/create files, auto-create directories |
| File Edit | ON | Search-and-replace with diff validation |
| Glob/Find | ON | File discovery by glob pattern |
| Grep/Search | ON | Content search by regex |
| Directory List | ON | List directory contents |
| Shell/Bash | ON | Execute commands with timeout and output capture |
| JS REPL | OFF | Persistent Node.js environment |
| Sub-agent Spawn | OFF | Delegate tasks to child agents |
| Plan/Todo | OFF | Track multi-step task progress |
| Request User Input | OFF | Structured multiple-choice questions |
| Web Fetch | OFF | Fetch and extract text from web pages |
| View Image | OFF | Read local images as base64 |
| Custom Tool Template | ON | Empty skeleton for domain-specific tools |
Harness Modules (architectural components)
| Module | Default | Description |
|---|
| Session Persistence | ON | JSONL append-only conversation log |
| ASCII Logo Banner | OFF | Custom ASCII art banner on startup — ask for project name |
| Context Compaction | OFF | Summarize older messages when context is long |
| System Prompt Composition | OFF | Assemble instructions from static + dynamic context |
| Tool Permissions / Approval | OFF | Gate dangerous tools behind user confirmation |
| Structured Event Logging | OFF | Emit events for tool calls, API requests, errors |
@-file References | OFF | @filename to attach file content to next message |
! Shell Shortcut | OFF | !command to run shell and inject output into context |
| Multi-line Input | OFF | Shift+Enter for multi-line (requires raw terminal mode) |
Slash Commands (user-facing REPL commands)
| Command | Default | Description |
|---|
/model | ON | Switch model via OpenRouter API |
/new | ON | Start a fresh conversation |
/help | ON | List available commands |
/compact | OFF | Manually trigger context compaction |
/session | OFF | Show session metadata and token usage |
/export | OFF | Save conversation as Markdown |
When slash commands are enabled, generate src/commands.ts with a command registry. See references/slash-commands.md for specs.
Visual Customization (present as single-select for each)
Input style — how the prompt looks. See references/input-styles.md:
| Style | Default | Description |
|---|
block | ON | Full-width background box with › prompt, adapts to terminal theme |
bordered | | Horizontal ─ lines above and below input |
plain | | Simple > readline prompt, no escape sequences |
| Other | | User describes what they want — implement a custom input style |
Tool display — how tool calls appear during execution. See references/tool-display.md:
| Style | Default | Description |
|---|
grouped | ON | Bold action labels with tree-branch output |
emoji | | Per-call ⚡/✓ markers with args and timing |
minimal | | Aggregated one-liner summaries |
hidden | | No tool output |
| Other | | User describes what they want — implement a custom display |
Loader animation — shown while waiting for model response. See references/loader.md:
| Style | Default | Description |
|---|
spinner | ON | Braille dot spinner (⠋⠙⠹…) to the left of the text |
gradient | | Scrolling color shimmer over the loader text |
minimal | | Trailing dots (Working···) |
| Other | | User describes what they want — implement a custom animation |
Also ask for the loader text (default: "Working").
Generation Workflow
After getting checklist selections, follow this workflow:
- [ ] Generate package.json with dependencies
- [ ] Generate src/config.ts (add showBanner field if ASCII Logo Banner is ON)
- [ ] Generate src/tools/index.ts wiring selected tools + server tools
- [ ] Generate selected tool files in src/tools/ (see Tool Pattern below, specs in references/tools.md)
- [ ] Generate src/agent.ts (core runner)
- [ ] Generate selected harness modules (specs in references/modules.md)
- [ ] Generate src/terminal-bg.ts (adaptive input background — see references/tui.md)
- [ ] Generate input style functions in src/cli.ts (block/bordered/plain — see references/input-styles.md)
- [ ] Generate src/renderer.ts (tool display — see references/tool-display.md)
- [ ] Generate src/loader.ts (loader animation — see references/loader.md)
- [ ] If slash commands selected: generate src/commands.ts (see references/slash-commands.md)
- [ ] If ASCII Logo Banner is ON: generate src/banner.ts (see ASCII Logo Banner section below)
- [ ] Generate src/cli.ts entry point (or src/server.ts — see references/server-entry-points.md)
- [ ] Generate .env.example with OPENROUTER_API_KEY=
- [ ] Verify: run npx tsc --noEmit to check types
Tool Pattern
All user-defined tools follow this pattern using @openrouter/agent/tool. Here is one complete example — all other tools in references/tools.md follow the same shape:
import { tool } from '@openrouter/agent/tool';
import { z } from 'zod';
import { readFile, stat } from 'fs/promises';
const DEFAULT_LINE_LIMIT = 2000;
const MAX_LINE_CHARS = 2000;
export const fileReadTool = tool({
name: 'file_read',
description:
'Read the contents of a file. Output is capped at 2000 lines by default (use offset/limit to paginate) and any line longer than 2000 characters is truncated. When the response is truncated, the hint field tells you how to continue.',
inputSchema: z.object({
path: z.string().describe('Absolute path to the file'),
offset: z.number().optional().describe('Start reading from this line (1-indexed)'),
limit: z.number().optional().describe(`Maximum lines to return (default ${DEFAULT_LINE_LIMIT})`),
}),
execute: async ({ path, offset, limit }) => {
try {
const content = await readFile(path, 'utf-8');
lines = content.();
start = offset ? offset - : ;
end = .(start + (limit ?? ), lines.);
longLines = ;
slice = lines.(start, end).( {
(line. <= ) line;
longLines++;
line.(, ) + ;
});
tailTruncated = end < lines.;
truncated = tailTruncated || longLines > ;
: [] = [];
(tailTruncated) hintParts.();
(longLines > ) hintParts.();
{
: slice.(),
: lines.,
...(truncated && {
: ,
...(tailTruncated && { : end + }),
: hintParts.(),
}),
};
} (: ) {
(err. === ) { : };
(err. === ) { : };
{ : err. };
}
},
});
For specs of all other tools, see references/tools.md.
Core Files
These files are always generated. The agent adapts them based on checklist selections.
package.json
Initialize the project and install dependencies at their latest versions:
npm init -y
npm pkg set type=module
npm pkg set scripts.start="tsx src/cli.ts"
npm pkg set scripts.dev="tsx watch src/cli.ts"
npm install @openrouter/agent glob zod
npm install -D tsx typescript @types/node
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
src/config.ts
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
export interface DisplayConfig {
toolDisplay: 'emoji' | 'grouped' | 'minimal' | 'hidden';
reasoning: boolean;
inputStyle: 'block' | 'bordered' | 'plain';
}
export interface AgentConfig {
apiKey: string;
model: string;
systemPrompt: string;
maxSteps: number;
maxCost: number;
sessionDir: string;
showBanner: boolean;
display: DisplayConfig;
slashCommands: boolean;
}
const DEFAULTS: AgentConfig = {
apiKey: '',
model: 'anthropic/claude-opus-4.7',
systemPrompt: [
'You are a coding assistant with access to tools for reading, writing, editing, and searching files, and running shell commands.',
'',
'Current working directory: {cwd}',
,
,
,
,
,
,
,
,
,
].(),
: ,
: ,
: ,
: ,
: { : , : , : },
: ,
};
(): {
config = { ... };
configPath = ();
((configPath)) {
file = .((configPath, ));
(file.) {
config. = { ...config., ...file. };
}
config = { ...config, ...file, : config. };
}
(process..) config. = process..;
(process..) config. = process..;
(process..) config. = (process..);
(process..) config. = (process..);
(overrides.) {
config. = { ...config., ...overrides. };
}
config = { ...config, ...overrides, : config. };
(!config.) ();
config;
}
src/tools/index.ts
Adapt imports based on checklist selections. This example includes all default-ON tools:
import { serverTool } from '@openrouter/agent';
import { fileReadTool } from './file-read.js';
import { fileWriteTool } from './file-write.js';
import { fileEditTool } from './file-edit.js';
import { globTool } from './glob.js';
import { grepTool } from './grep.js';
import { listDirTool } from './list-dir.js';
import { shellTool } from './shell.js';
export const tools = [
fileReadTool,
fileWriteTool,
fileEditTool,
globTool,
grepTool,
listDirTool,
shellTool,
serverTool({ type: 'openrouter:web_search' }),
serverTool({ type: 'openrouter:datetime', parameters: { timezone: 'UTC' } }),
];
src/agent.ts
import { OpenRouter } from '@openrouter/agent';
import type { Item } from '@openrouter/agent';
import { stepCountIs, maxCost } from '@openrouter/agent/stop-conditions';
import type { AgentConfig } from './config.js';
import { tools } from './tools/index.js';
export type ChatMessage = { role: 'user' | 'assistant' | 'system'; content: string };
export type AgentEvent =
| { type: 'text'; delta: string }
| { type: 'tool_call'; name: string; callId: string; args: Record<string, unknown> }
| { type: 'tool_result'; name: string; callId: string; output: string }
| { : ; : };
() {
client = ({ : config. });
result = client.({
: config.,
: config..(, process.()),
: input | [],
tools,
: [(config.), (config.)],
});
(options?.) {
textByItem = <, >();
callNames = <, >();
( item result.()) {
(options?.?.) ;
(item. === ) {
text = item.
?.((c): c is { : ; : } => c)
.( c.)
.() ?? ;
prev = textByItem.(item.) ?? ;
(text. > prev) {
options.({ : , : text.(prev) });
textByItem.(item., text.);
}
} (item. === ) {
callNames.(item., item.);
(item. === ) {
args = ( { { item. ? .(item.) : {}; } { {}; } })();
options.({ : , : item., : item., args });
}
} (item. === ) {
out = item. === ? item. : .(item.);
options.({
: ,
: callNames.(item.) ?? ,
: item.,
: out. > ? out.(, ) + : out,
});
} (item. === ) {
text = item.?.( s.).() ?? ;
(text) options.({ : , : text });
}
}
}
response = result.();
{ : response. ?? , : response., : response. };
}
() {
( attempt = , max = options?. ?? ; attempt <= max; attempt++) {
{ (config, input, options); }
(: ) {
s = err?. ?? err?.;
(!(s === || (s >= && s < )) || attempt === max) err;
( (r, .( * ** attempt, )));
}
}
();
}
src/cli.ts
Three input styles are supported: block (background box), bordered (horizontal lines), and plain (simple caret). See references/input-styles.md for full implementations of styledReadLine(), borderedReadLine(), and the getInput() dispatcher.
import { createInterface } from 'readline';
import { loadConfig } from './config.js';
import { runAgentWithRetry, type AgentEvent } from './agent.js';
import { detectBg } from './terminal-bg.js';
const DIM = '\x1b[2m';
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const CYAN = '\x1b[36m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const GRAY = '\x1b[90m';
function formatTokens(n: number): string {
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
}
function summarizeArgs(: , : <, >): {
key = { : , : , : ,
: , : , : , : ,
}[name] ?? .(args)[];
(!key || !(key args)) ;
val = (args[key]);
;
}
() {
config = ();
= config.. === ? () : ;
width = .(process.. || , );
line = + .(width) + ;
.();
.();
.();
(config.) .();
.();
rl = ({ : process., : process., : });
(): <> {
(config..) {
: ();
: ();
:
:
( { rl.(); rl.(, r); });
}
}
() {
input = ();
trimmed = input.();
(!trimmed) ;
(config.. !== ) {
cwd = process.().(process.. ?? , );
process..();
}
(trimmed.() === ) { process.(); }
.();
streaming = , started = ;
toolStart = <, >();
dots = [, , ];
di = ;
spin = ( {
(!started) process..();
}, );
= () => {
(!started) { started = ; process..(); }
(event. === ) { streaming = ; process..(event.); }
(event. === ) {
(streaming) { process..(); streaming = ; }
toolStart.(event., .());
args = (event., event.);
.();
} (event. === ) {
ms = .() - (toolStart.(event.) ?? .());
.();
started = ;
}
};
{
result = (config, trimmed, { : handleEvent });
(spin);
(streaming) process..();
inT = result.?. ?? ;
outT = result.?. ?? ;
.();
} (: ) {
(spin);
(streaming) process..();
.();
}
}
}
();
ASCII Logo Banner
When ASCII Logo Banner is selected, ask the user for their project name, then generate src/banner.ts with ASCII art of that name. Use a block-letter style with the █ character for the art. The banner should fit in a 60-column terminal.
src/banner.ts
Generate ASCII art for the user's project name. Example for a project called "ACME":
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const DIM = '\x1b[2m';
const CYAN = '\x1b[36m';
const LOGO = `
█████╗ ██████╗███╗ ███╗███████╗
██╔══██╗██╔════╝████╗ ████║██╔════╝
███████║██║ ██╔████╔██║█████╗
██╔══██║██║ ██║╚██╔╝██║██╔══╝
██║ ██║╚██████╗██║ ╚═╝ ██║███████╗
╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝`;
export function printBanner(model: string): void {
console.log(CYAN + BOLD + LOGO + RESET);
console.log(` ${DIM}model ${RESET}${model}\n`);
}
Adapt the ASCII art to the user's actual project name. Keep it to one or two short words that fit in 60 columns.
Wire into src/cli.ts
Add at the top of main(), before the text banner, when showBanner is selected:
import { printBanner } from './banner.js';
if (config.showBanner) {
printBanner(config.model);
} else {
}
Add showBanner: boolean to AgentConfig (default false). Enable via agent.config.json or loadConfig({ showBanner: true }).
Reference Files
For content beyond the core files:
- references/tools.md — Specs for all user-defined tools: file-read, file-write, file-edit, glob, grep, list-dir, shell, js-repl, sub-agent, plan, request-input, web-fetch, view-image, custom template
- references/modules.md — Harness modules: session persistence, context compaction, system prompt composition, tool approval, structured logging
- references/tui.md — Terminal background detection, adaptive input background
- references/tool-display.md — Tool display styles: emoji, grouped, minimal; TuiRenderer class, per-tool colors, formatters
- references/input-styles.md — Input styles: block (background box), bordered (horizontal lines), plain (simple caret)
- references/loader.md — Loader animations: gradient (scrolling shimmer), spinner (braille dots), minimal (trailing dots)
- references/slash-commands.md — Slash command registry: /model, /new, /help, /compact, /session, /export
- references/system-prompt.md — Default system prompt, buildSystemPrompt(), customization guide
- references/server-entry-points.md — Express/Hono API server entry point with SSE streaming, plus extension points (MCP, WebSocket, dynamic models)