A minimal TypeScript app that trims prompts to fit a token budget
Welcome to another official UVF IT guide! In this quick‑fire tutorial you’ll build a tiny clone of the popular *context‑mode* library – a tool that trims a list of messages so they fit inside a configurable token window.
We’ll use plain Node.js, TypeScript and a local SQLite DB, all on the free tier. In ~45 minutes you’ll have a runnable CLI that accepts a JSON file of messages and returns the optimal subset.
Create a fresh npm project, add TypeScript and SQLite packages, and generate a basic tsconfig.
mkdir context-opt-clone && cd context-opt-clone
npm init -y
npm install typescript better-sqlite3
npm install --save-dev @types/node
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --module es2020 --target es2020Define a simple Message interface and a tiny SQLite helper that loads messages from a JSON file into a temporary table.
src/db.ts
import Database from 'better-sqlite3';
import { readFileSync } from 'fs';
export interface Message {
id: number;
role: 'system' | 'user' | 'assistant';
content: string;
}
/**
* Loads an array of Message objects from a JSON file and stores them in a temporary
* in‑memory SQLite table called `messages`. Returns the DB instance for further queries.
*/
export function initMessageDb(jsonPath: string): Database {
const raw = readFileSync(jsonPath, 'utf-8');
const msgs: Message[] = JSON.parse(raw);
const db = new Database(':memory:');
db.exec(`CREATE TABLE messages (id INTEGER PRIMARY KEY, role TEXT, content TEXT);`);
const insert = db.prepare('INSERT INTO messages (id, role, content) VALUES (?, ?, ?)');
const insertMany = db.transaction((list: Message[]) => {
for (const m of list) insert.run(m.id, m.role, m.content);
});
insertMany(msgs);
return db;
}In this step we add the heart of the clone: a simple greedy trim that respects a word budget. The function walks the message list from newest to oldest, accumulating words until the limit would be exceeded, then stops. By keeping the newest entries we mimic how LLMs usually prioritize recent context. This logic lives in src/optimizer.ts and will be imported by the CLI later.
src/optimizer.ts
export interface Message {
role: string;
content: string;
}
/**
* Trim an array of messages so that the total word count does not exceed `maxWords`.
* The newest messages (end of the array) are kept while older ones are dropped.
*/
export function trimMessages(messages: Message[], maxWords: number): Message[] {
const result: Message[] = [];
let wordCount = 0;
// iterate from newest to oldest
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
const words = msg.content.split(/\s+/).filter(Boolean).length;
if (wordCount + words > maxWords) break;
result.unshift(msg); // prepend to preserve original order
wordCount += words;
}
return result;
}Now we connect the pieces: the CLI reads a JSON payload, feeds it to the optimizer, and prints the trimmed list. The script expects two arguments – the path to the input file and the maximum word budget. It uses Node's fs module for I/O and re‑exports the trimMessages function from the optimizer we just wrote. Running this file will give us immediate feedback on how the algorithm behaves.
src/cli.ts
import { readFileSync } from 'fs';
import { trimMessages, Message } from './optimizer';
function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: node cli.js <input.json> <maxWords>');
process.exit(1);
}
const [inputPath, maxWordsStr] = args;
const maxWords = parseInt(maxWordsStr, 10);
if (isNaN(maxWords) || maxWords <= 0) {
console.error('maxWords must be a positive integer');
process.exit(1);
}
const raw = readFileSync(inputPath, 'utf-8');
const messages: Message[] = JSON.parse(raw);
const trimmed = trimMessages(messages, maxWords);
console.log(JSON.stringify(trimmed, null, 2));
}
main();With the code in place we compile the TypeScript sources and execute the CLI against a sample payload. The build step uses the TypeScript compiler (tsc) and outputs JavaScript into the dist folder. Finally we run the compiled CLI, passing a JSON file that contains a series of messages and a word limit, and we should see a trimmed JSON array printed to the console.
# install TypeScript locally if not already present
npm install typescript@latest --save-dev
# compile the project (tsconfig.json should point to src/**/*.ts)
npx tsc
# run the CLI against a sample payload (create sample.json beforehand)
node dist/cli.js sample.json 50| Check | Command / Why it matters |
|---|---|
| Project builds without errors | npx tsc finishes with no output |
| CLI prints a JSON array | Running the command in Phase 5 shows trimmed messages |
| Word count of output ≤ limit | Count words in the printed content fields; should not exceed the number passed |
Your minimal Context‑Window Optimizer is now ready. Feel free to replace the word‑count heuristic with a real token estimator or persist messages in a real DB. Happy hacking!