LocalLens

7. The CLI

A 30-line entry point that proves the AI core without any UI.

src/cli.ts is the smallest entry point LocalLens has. It reads two argv values, instantiates LocalLensApp, runs one round-trip, cleans up.

The full file

src/cli.ts
import type { Brain } from "./domain.ts";
import { LocalLensApp } from "./locallens.ts";

const [folderPath, ...questionParts] = Bun.argv.slice(2);
const question = questionParts.join(" ").trim();

if (!folderPath || !question) {
  console.log('Usage: bun run cli <folder-path> "question about the folder"');
  process.exit(1);
}

const app = new LocalLensApp();
let brain: Brain | undefined;

try {
  brain = await app.createBrainFromFolder({
    name: folderPath.split(/[\\/]/).filter(Boolean).at(-1) ?? "cli-brain",
    folderPath,
  });

  const result = await app.askBrain(brain.id, question);
  console.log(`\n${result.answer}\n`);

  if (result.citations.length > 0) {
    console.log("Sources:");
    for (const citation of result.citations) {
      console.log(`- ${citation.relativePath}#${citation.chunkIndex}`);
    }
  }
} finally {
  if (brain) await app.deleteBrain(brain.id).catch(() => undefined);
  await app.close();
}

The temporary-brain pattern

The CLI is a one-shot tool. It creates a brain, uses it, and deletes it before exiting. That's why the cleanup lives in finally:

} finally {
  if (brain) await app.deleteBrain(brain.id).catch(() => undefined);
  await app.close();
}

Two reasons:

  1. No drift in the JSON store. Running the CLI a hundred times doesn't leave a hundred entries in .locallens/store.json.
  2. No leftover workspaces. The QVAC workspace closes and deletes alongside the brain, so disk usage stays bounded.

.catch(() => undefined) on deleteBrain is intentional. If the brain creation step itself failed, close() still needs to run. Swallowing the cleanup error keeps the original throw on its way up.

Brain naming

name: folderPath.split(/[\\/]/).filter(Boolean).at(-1) ?? "cli-brain",

The CLI derives the brain name from the last segment of the folder path. examples/sample-brain becomes "sample-brain". The ?? "cli-brain" fallback covers paths like /. Just enough to make the JSON entry readable while the brain briefly exists.

Output formatting

The CLI prints the answer and a Sources: block. No Markdown rendering, no streaming display, no progress bar. That's deliberate — the CLI is the simplest proof that the pipeline works. Nicer interactions live in the browser app.

LocalLens uses QWEN3_1_7B_INST_Q4 because it offers a strong balance of answer
quality and local resource use [1]. A 600M fallback is wired in for slimmer
machines [2].

Sources:
- locallens.md#0
- qvac-notes.md#1

Exit codes

  • 1 — missing argument(s). The usage line is printed.
  • Non-zero on any thrown error from LocalLensApp. Bun propagates the throw by default.

AppErrors from the workflow surface as ordinary stack traces here. No HTTP response to format, so the status field is unused.

When you'd add flags

The CLI is intentionally argv-only. If you want named flags (--top-k, --model, --keep), keep them in this file. Don't push them into LocalLensApp. The workflow stays the same; the CLI just learns more ways to invoke it.

Next: the Bun server, the longer-lived counterpart.

On this page