Build an Interactive CLI Chat Loop with Flue and Node.js
Step-by-step tutorial to build an interactive terminal chat application using the Flue runtime with tool calling, skills, and SQLite persistence.
Published on • September 18, 2026
AI Assistant

This tutorial walks you through building an interactive chat loop in the terminal using the Flue runtime. You’ll have a fully functional CLI chat app with tool calling, skills, and conversation persistence by the end.
Architecture Overview
The app connects a terminal readline loop to a Flue agent:
graph TD
A["Terminal (chat.ts)"] --> B[readline input loop]
B --> C["init(Assistant)"]
C --> D[agent instance]
D --> E["agent.dispatch(message)"]
E --> F["agent.read(receipt)"]
F --> G["src/agents/assistant.ts"]
G --> H["useModel('gemini-2.5-flash')"]
G --> I["useTool(weather)"]
G --> J["useSkill(itPolicy)"]
style A fill:#f9f,stroke:#333,stroke-width:2px
style D fill:#bbf,stroke:#333,stroke-width:2px
style G fill:#bfb,stroke:#333,stroke-width:2px
The data flow between user, CLI, and agent:
sequenceDiagram
participant U as User (Terminal)
participant C as CLI (chat.ts)
participant A as Agent
U->>C: "You: " + message
C->>A: agent.dispatch(message)
A-->>C: receipt
C->>A: agent.read(receipt)
A-->>C: SSE stream + tool calls
C-->>U: "Assistant: " + reply.text
Step 1 — Project Setup
Install Dependencies
npm create flue-app my-cli-chat
cd my-cli-chat
npm install
Required dependencies in package.json:
{
"dependencies": {
"@flue/runtime": "^2.0.5",
"@flue/sdk": "^2.0.8",
"hono": "^4.13.8"
},
"devDependencies": {
"@flue/cli": "^2.0.5",
"@flue/vite": "^2.0.6",
"cross-env": "^10.1.0",
"tsx": "^4.23.13",
"typescript": "^7.0.2",
"vite": "^8.3.0"
}
}
Add Scripts to package.json
{
"scripts": {
"chat": "cross-env NODE_OPTIONS=--no-warnings tsx src/chat.ts",
"dev": "vite dev",
"build": "vite build",
"check:types": "tsc --noEmit"
}
}
Configure TypeScript
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["node"],
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}
Step 2 — Create the Agent
Create src/agents/assistant.ts:
'use agent';
import { useModel, useSandbox, useSkill, useTool } from '@flue/runtime';
import { local } from '@flue/runtime/node';
import { weather } from '../tools/weather.ts';
import { itPolicy } from '../skills/it-policy.ts';
export function Assistant() {
useModel('google/gemini-2.5-flash');
useSandbox(local());
useSkill(itPolicy);
useTool(weather);
return `You are a helpful AI assistant with access to real-time weather data and company IT policies.
## Capabilities
- Look up current weather for any location using the weather tool
- Answer questions about company IT policies, security guidelines, and best practices
## Response Style
- Be concise and direct — aim for 2-4 sentences unless detail is requested
- Use plain text formatting
- Cite sources when referencing IT policy information`;
}
Create the Weather Tool
Create src/tools/weather.ts:
import { defineTool } from '@flue/runtime/tool';
import * as v from 'valibot';
export const weather = defineTool({
name: 'get_weather',
description: 'Get the current weather and forecast for a location using wttr.in.',
input: v.object({
location: v.string(),
}),
async run({ data }) {
const res = await fetch(
`http://wttr.in/${encodeURIComponent(data.location)}?format=j1`,
);
if (!res.ok) {
return `Failed to fetch weather for "${data.location}". Status: ${res.status}`;
}
const json: any = await res.json();
const current = json.current_condition?.[0];
if (!current) return `No weather data for "${data.location}".`;
return [
`Location: ${json.nearest_area?.[0]?.areaName?.[0]?.value ?? data.location}`,
`Temperature: ${current.temp_C}°C`,
`Condition: ${current.weatherDesc?.[0]?.value ?? 'Unknown'}`,
`Humidity: ${current.humidity}%`,
`Wind: ${current.windspeedKmph} km/h`,
].join('\n');
},
});
Create the IT Policy Skill
Create src/skills/it-policy.ts:
import { defineSkill } from '@flue/runtime';
export const itPolicy = defineSkill({
name: 'it-policy',
description: 'IT and security policy knowledge base.',
instructions: `
## IT Policy
### Password & Authentication
- Minimum 12 characters with uppercase, lowercase, number, and symbol
- Enable MFA on all accounts that support it
### Device Policy
- Company-issued laptops only
- Full-disk encryption required
- Always use VPN on public networks
`.trim(),
});
Step 3 — Build the Chat Loop
Create src/chat.ts:
import { init } from '@flue/runtime';
import { sqlite, start } from '@flue/runtime/node';
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { Assistant } from './agents/assistant.ts';
const conversationId = process.argv[2] ?? `chat-${Date.now()}`;
const flue = await start({
agents: [Assistant],
db: sqlite('./data/flue.db'),
});
const agent = init(Assistant, { id: conversationId });
const rl = readline.createInterface({ input, output });
console.log(`Chat started (conversation: ${conversationId})`);
console.log('Type "quit" or press Ctrl+C to exit.\n');
try {
while (true) {
const message = await rl.question('You: ');
const trimmed = message.trim();
if (!trimmed) continue;
if (trimmed === 'quit' || trimmed === 'exit') break;
const receipt = await agent.dispatch(trimmed);
const reply = await agent.read(receipt);
console.log(`\nAssistant: ${reply.text}\n`);
}
} catch (err: any) {
if (err.code !== 'SIGINT' && err.code !== 'ERR_USE_AFTER_CLOSE') throw err;
} finally {
rl.close();
await flue.stop();
console.log('\nGoodbye!');
}
Code Breakdown
1. Imports and Runtime Setup
import { init } from '@flue/runtime';
import { sqlite, start } from '@flue/runtime/node';
init()— Creates an agent instance from the agent functionstart()— Starts the Flue runtime (registers agents, opens DB connection)sqlite()— Persistence adapter that stores conversations in SQLite
2. Conversation ID
const conversationId = process.argv[2] ?? `chat-${Date.now()}`;
- Accepts a conversation ID from the command line, or generates a new one
- Enables resuming old conversations:
npm run chat -- my-conv-id
3. Start the Runtime
const flue = await start({
agents: [Assistant],
db: sqlite('./data/flue.db'),
});
agents— Registers the agent functions to usedb— Persistence adapter (stores data in./data/flue.db)
4. Create the Agent Instance
const agent = init(Assistant, { id: conversationId });
init(Assistant, { id })— Creates an instance of the Assistant agentid— Conversation identifier for persisting state
5. Readline Interface
const rl = readline.createInterface({ input, output });
- Creates an interface for reading user input from the terminal
6. The Chat Loop
while (true) {
const message = await rl.question('You: ');
const trimmed = message.trim();
if (!trimmed) continue; // Skip empty input
if (trimmed === 'quit' || trimmed === 'exit') break; // Exit loop
const receipt = await agent.dispatch(trimmed); // Send message to agent
const reply = await agent.read(receipt); // Read response
console.log(`\nAssistant: ${reply.text}\n`); // Display result
}
Internal flow of each iteration:
flowchart TD
A["rl.question('You: ')"] --> B{Empty input?}
B -->|Yes| A
B -->|No| C{quit/exit?}
C -->|Yes| D[Break loop]
C -->|No| E["agent.dispatch(trimmed)"]
E --> F["agent.read(receipt)"]
F --> G["reply.text → print"]
G --> A
style E fill:#bbf,stroke:#333,stroke-width:2px
style G fill:#bfb,stroke:#333,stroke-width:2px
7. Error Handling
catch (err: any) {
if (err.code !== 'SIGINT' && err.code !== 'ERR_USE_AFTER_CLOSE') throw err;
}
SIGINT— User pressed Ctrl+CERR_USE_AFTER_CLOSE— Readline was already closed
8. Cleanup
finally {
rl.close(); // Close readline interface
await flue.stop(); // Stop runtime + DB connection
console.log('\nGoodbye!');
}
Run the Chat Loop
npm run chat
Example Session
$ npm run chat
Chat started (conversation: chat-1726684800000)
Type "quit" or press Ctrl+C to exit.
You: What's the weather in Bangkok?
Assistant: Bangkok is currently 32°C with partly cloudy skies. Humidity is at 78% with winds at 8 km/h from the south.
You: What's our company's VPN policy?
Assistant: According to company IT policy, VPN (WireGuard) is required when accessing internal resources. Always use VPN on public or home networks.
You: quit
Goodbye!
Resume a Previous Conversation
npm run chat -- chat-1726684800000
Project Structure
graph TD
A["my-cli-chat/"] --> B[package.json]
A --> C[tsconfig.json]
A --> D["src/"]
D --> E["chat.ts — CLI chat loop entry"]
D --> F["agents/"]
F --> G["assistant.ts — Agent definition"]
D --> H["skills/"]
H --> I["it-policy.ts — Knowledge base"]
D --> J["tools/"]
J --> K["weather.ts — Weather tool"]
style A fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#bbf,stroke:#333,stroke-width:2px
style G fill:#bfb,stroke:#333,stroke-width:2px
style K fill:#fbf,stroke:#333,stroke-width:2px
Full Execution Flow
When a user sends a message, here’s what happens inside the Flue runtime:
flowchart TD
A[User input] --> B["readline.question()"]
B --> C["agent.dispatch(text)"]
C --> D["Flue Runtime"]
D --> E["System prompt + User message"]
E --> F[LLM inference]
F --> G{Tool call needed?}
G -->|Yes| H[Execute tool]
H --> I[Return result to LLM]
I --> F
G -->|No| J[Final response]
J --> K["agent.read(receipt)"]
K --> L["console.log(reply.text)"]
style D fill:#e6f3ff,stroke:#333,stroke-width:2px
style F fill:#bbf,stroke:#333,stroke-width:2px
style H fill:#bfb,stroke:#333,stroke-width:2px
style J fill:#fbf,stroke:#333,stroke-width:2px
Key APIs Reference
| API | Purpose |
|---|---|
start({ agents, db }) | Start the Flue runtime |
init(Agent, { id }) | Create an agent instance |
agent.dispatch(message) | Send a message to the agent, returns a receipt |
agent.read(receipt) | Read the response from a receipt |
reply.text | The text content of the agent’s reply |
flue.stop() | Shut down the runtime and clean up |
defineTool({ ... }) | Define a tool for the agent |
defineSkill({ ... }) | Define a skill (knowledge base) |
useModel('provider/model') | Set the LLM model |
useTool(tool) | Register a tool with the agent |
useSkill(skill) | Register a skill with the agent |
Next Steps
From here you can extend the CLI app by:
- Adding more tools (database queries, API calls, file operations)
- Creating additional skills for different knowledge domains
- Implementing streaming output for token-by-token responses
- Adding command-line flags for model selection or debug mode
- Exporting conversation history to JSON or Markdown
The Flue runtime handles agent execution, tool orchestration, and conversation persistence. Your job is to define what the agent can do and wire up the terminal input/output.