Context‑Window Optimizer Clone (TypeScript)

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.

Before you start…

Clone Architecture
LOCAL RUNTIME CLI Entrypoint (TS) SQLite (messages) Window Optimizer
1

Phase 1: Scaffold & Init

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 es2020
2

Phase 2: Message Model & DB

Define 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;
}
3

Phase 3: Window Optimizer Logic

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;
}
4

Phase 4: CLI Glue

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();
5

Phase 5: Run & Verify

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

Verifying the Stack

CheckCommand / Why it matters
Project builds without errorsnpx tsc finishes with no output
CLI prints a JSON arrayRunning the command in Phase 5 shows trimmed messages
Word count of output ≤ limitCount words in the printed content fields; should not exceed the number passed

You did it!

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!