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
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:
- No drift in the JSON store. Running the CLI a hundred
times doesn't leave a hundred entries in
.locallens/store.json. - 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#1Exit 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.