helpers/ — CLI Chrome & LLM Wrappers
The infrastructure boundary between every example.js and the outside world (terminal, OpenAI API, llama.cpp). Centralises chalk-coloured output, ora spinners, and minimal LLM clients so lessons can focus on the concept they teach.
3 files 152 LOC chalk + ora Used by every lesson
📚 What is this & why does it exist?
helpers/ is the infrastructure boundary between every lesson and the outside world (terminal, OpenAI, llama.cpp). Without it, every example.js would re-implement chalk-coloured headers, ora spinners, OpenAI API boilerplate, and llama.cpp model-loading. By centralising these, the lessons can focus on the concept being taught without drowning in setup boilerplate.
💡 Real-world analogy
Picture a cooking show kitchen. The aprons, the stage lights, the camera angles, the chyron at the bottom of the screen — none of it is the recipe. But without it, the chef would be cooking in a dark, unlabelled kitchen and you couldn't follow along. helpers/ is that production crew. Critically, you don't ship the production crew with the recipe book (src/) — the crew exists for the demos, not the dish.
🎯 Why This Module Exists
helpers/ exists because every lesson needs the same chrome and the same minimal LLM client, but neither belongs in src/. The src/ library is what a downstream consumer imports; helpers are what the demos rely on for chrome and CLI ergonomics. They're stylistic, not architectural — and that distinction is enforced by directory placement.
Without this module: every example.js would carry its own try/catch wrapping for timing, its own chalk-coloured headers, its own ora spinner setup, its own OpenAI client construction. We'd see 50 lines of identical boilerplate at the top of each lesson. With helpers/, the lessons are 50 lines shorter and visually consistent.
🏗️ Design Decisions
| Decision | Choice Made | Alternatives | Why |
|---|---|---|---|
OutputHelper as static class | All methods static, no state | Module of free functions; instance-based logger | Static keeps call-sites short (OutputHelper.log.info(…)); namespace-pattern common in JS demos. |
Two LLM wrappers (LlamaPrompter + src/llms/LlamaCpp) | Coexisting | Pick one | LlamaPrompter is simpler — 39 lines, no BaseLLM inheritance, no grammar plumbing. LlamaCpp is what a library exposes. |
OpenAI via responses.create | New "Responses API" | Chat completions API | Matches OpenAI's most recent recommended interface. |
dotenv/config import side-effect | At top of openai-prompter.js | Manual process.env checks | Reads .env automatically — every lesson "just works" if the user copied .env_example. |
| Spinners via ora | withSpinner(message, fn) higher-order | Inline console.log | Long ops show progress; failure auto-reports spinner.fail(msg). |
Higher-order runExample(title, fn) | Wraps every lesson body | Inline try/catch + timing per lesson | Visual consistency across 15 lessons; automatic timing; error capture. |
Architecture
classDiagram
class OutputHelper {
+divider
+createHeader(title, subtitle)
+formatStats(stats)
+formatChunkPreview(chunk, i)
+formatStrategyComparison(name, ...)
+log: {success, info, warn, error}
+withSpinner(msg, fn)
+analyzeChunks(chunks)
+runExample(title, fn, subtitle)
}
class OpenAIClient {
+client: OpenAI
+model: string
+constructor(apiKey, model)
+send(input)
}
class LlamaPrompter {
+modelPath: string
+llama / model / context / session
+init()
+prompt(message)
+dispose()
}
class chalk
class ora
class openai
class node_llama_cpp
OutputHelper ..> chalk
OutputHelper ..> ora
OpenAIClient ..> openai
LlamaPrompter ..> node_llama_cpp
A Lesson's Output Pipeline
sequenceDiagram
participant E as example.js
participant OH as OutputHelper
participant OR as ora
participant CH as chalk
participant T as Terminal
E->>OH: runExample("Title", async () => {...})
OH->>OH: createHeader(title)
OH->>CH: chalk.bold(title) + dim subtitle
CH-->>T: header lines
OH->>E: invoke fn()
E->>OH: withSpinner("loading model", loadFn)
OH->>OR: ora("loading model").start()
OR-->>T: [spin] loading model (animated)
OH->>E: await loadFn()
E-->>OH: model
OH->>OR: spinner.succeed()
OR-->>T: [done] loading model
E->>OH: formatStats({size: 1024, ...})
OH->>CH: chalk.cyan(value)
CH-->>T: stats table
OH-->>E: completed
OH->>CH: chalk.green("Completed in Xs")
CH-->>T: success line
Files
| File | LOC | Role & Why It Exists |
|---|---|---|
helpers/output-helper.js | 85 | OutputHelper static class. Wraps chalk + ora. runExample() is the demo shell. analyzeChunks() reports avg/min/max/median chunk size. |
helpers/openai-prompter.js | 28 | OpenAIClient thin wrapper around openai.responses.create(). Reads OPENAI_API_KEY from .env. Used optionally in 06/05. |
helpers/llama-prompter.js | 39 | LlamaPrompter simpler local-LLM wrapper. Used directly by 06/05 query rewriting. Coexists with src/llms/LlamaCpp. |
📝 Annotated Code Walkthrough — OutputHelper.runExample
The single most-reused function in the project. It's the demo shell that wraps every example.js body.
Render the header
static async runExample(title, fn, subtitle = '') {
this.createHeader(title, subtitle);
Why first? Because if fn() throws immediately, the reader still sees what lesson they were attempting. Without this, a missing GGUF would just print a stack trace with no context.
Capture start time
const t0 = Date.now();
Wall-clock timing. performance.now() would be more precise but the lessons are second-grain — Date.now() is plenty.
Run the lesson body
try {
await fn();
The lesson body is async-by-construction (every interesting RAG operation is async). The try/catch is what turns a stack trace into a friendly red line.
Success path: report timing
console.log(chalk.green(`\nCompleted in ${(Date.now() - t0) / 1000}s\n`));
Green on success matches every other tool the reader will encounter (npm, pytest, etc.). The trailing newline gives the next lesson room to breathe.
Failure path: red message
} catch (e) {
console.error(chalk.red(`Failed: ${e.message}`));
}
}
Note e.message only — the full stack would dominate the terminal. The reader gets the gist; process.env.DEBUG would let them re-run with full trace if they wanted (not implemented today).
analyzeChunks(chunks) calls sizes.sort((a,b) => a-b) in-place. If the caller reuses sizes after, it's been mutated. The lessons throw the array away immediately, so this never bites — but it's a code smell to remember if the helper is generalised.
⚠ Edge Cases & Boundaries
| Scenario | What Happens |
|---|---|
OPENAI_API_KEY not in .env | OpenAIClient constructor throws "Missing OPENAI_API_KEY environment variable." — user follows .env_example. |
LlamaPrompter.prompt() called before init | Auto-inits on first call (if (!this.session) await this.init()) — lazy by design. |
| Spinner fails because terminal is non-interactive | ora detects TTY; degrades to plain text. No special handling needed in helpers. |
runExample body throws non-Error | e.message is undefined — minor cosmetic issue. Acceptable for demos. |
Directory Map
helpers/ (flat, 3 files)
helpers/
├── output-helper.js (85 LOC) ← OutputHelper static class; chalk + ora wrappers
├── openai-prompter.js (28 LOC) ← OpenAIClient thin wrapper around responses API
└── llama-prompter.js (39 LOC) ← LlamaPrompter simpler local LLM (used by 06/05)
Configuration
| Knob | Default | Description |
|---|---|---|
OutputHelper.divider | '─' × 80 | Header separator. Static; not configurable per call. |
OpenAIClient(apiKey, model) | process.env.OPENAI_API_KEY, "gpt-4o" | API key from env; default model overridable. |
LlamaPrompter(modelRelativePath) | "../models/hf_Qwen_Qwen3-1.7B.Q8_0.gguf" | Path resolved relative to the helper file. |
| chalk colours | green=ok, blue=info, yellow=warn, red=err, cyan=value, dim=subtitle | Hard-coded for visual consistency across lessons. |
Code Examples
Wrap a lesson body
import { OutputHelper } from '../helpers/output-helper.js';
await OutputHelper.runExample('Lesson 03 — Text Splitting', async () => {
// ... lesson body ...
const chunks = await splitter.splitDocuments(docs);
OutputHelper.formatStats(OutputHelper.analyzeChunks(chunks));
}, 'Compare three splitting strategies on the same PDF');
Long-running operation with spinner
const model = await OutputHelper.withSpinner(
'Loading bge-small-en-v1.5...',
async () => await llama.loadModel({ modelPath: MODEL_PATH })
);
Quick local LLM prompt (used in 06/05)
import { LlamaPrompter } from '../../../helpers/llama-prompter.js';
const lp = new LlamaPrompter();
const rewritten = await lp.prompt('Rewrite this query as 3 paraphrases: "cheap red shoes"');
console.log(rewritten);
await lp.dispose();