0. Project setup
Initialize a Bun + TypeScript project, install QVAC, and pin the dev tools you'll use for the rest of the walkthrough.
Before any source files exist, you need a working Bun + TypeScript
project. This step takes about a minute and produces the shape the
later src/cli.ts and src/server.ts will run inside.
Prerequisites
- Bun
>=1.3.0(install) - Node
>=22.17.0(used by TypeScript and a fewnode:modules)
No OpenAI key. No Pinecone account. No GPU. Everything runs locally through QVAC.
1. Create the project
mkdir locallens
cd locallens
bun init -ybun init -y writes a minimal package.json, tsconfig.json,
.gitignore, and a placeholder index.ts. Delete index.ts — the
real entry points (src/cli.ts, src/server.ts) come later.
2. Install dependencies
bun add @qvac/sdk
bun add -d typescript @types/bun @biomejs/biomeWhat each one is for:
| Package | Why |
|---|---|
@qvac/sdk | The local AI loop: model loading, RAG, completion. The only runtime dependency LocalLens has. |
typescript | Strict types for the eight files you're about to write. |
@types/bun | Type definitions for Bun.argv, Bun.serve, Bun.file, import.meta.dir. |
@biomejs/biome | One tool for linting and formatting. Replaces ESLint + Prettier. |
That's the whole dependency surface. No vector database, no embedding library, no chunker.
3. Lock the runtime versions
Open the freshly created package.json and set type: "module"
plus the engines field, so anyone cloning the repo gets the same
runtime:
{
"name": "locallens",
"version": "0.1.0",
"type": "module",
"private": true,
"license": "MIT",
"engines": {
"bun": ">=1.3.0",
"node": ">=22.17.0"
}
}4. Add the scripts you'll use
Add these to package.json. Each one gets wired up by a later
step. For now they just need to exist:
{
"scripts": {
"cli": "bun run src/cli.ts",
"dev": "bun run src/server.ts",
"start": "bun run src/server.ts",
"build": "bun build src/server.ts --target=bun --outdir=dist",
"test": "bun test",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"format": "biome format --write .",
"check": "bun run lint && bun run typecheck && bun run test && bun run build"
}
}bun run check is the composite command CI uses. It runs lint →
types → tests → build, in that order, and exits non-zero if any
step fails.
5. Tighten tsconfig.json
bun init produces a reasonable default. The walkthrough wants a
few extras: strict mode, Bun types, and the ability to import .ts
extensions explicitly. Replace the generated file with this:
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["bun"],
"allowImportingTsExtensions": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}Two flags worth flagging:
allowImportingTsExtensions: truelets you writeimport "./domain.ts"in source files. Bun runs them directly; the explicit extension keepstschappy.verbatimModuleSyntax: truerequiresimport typefor type-only imports. The walkthrough code is written this way. This flag enforces it.
6. Configure Biome
Biome handles linting and formatting. One config file:
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"files": {
"includes": ["**", "!dist", "!node_modules", "!.locallens"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": { "recommended": true }
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always",
"trailingCommas": "all"
}
}
}The !.locallens exclusion matters once the app is running. That
folder is where the JSON store and brain data live, and Biome
shouldn't try to format it.
7. Configure QVAC
QVAC reads its runtime config from qvac.config.json at the repo
root. Create it now so the gateway has something to load against:
{
"loggerLevel": "info",
"loggerConsoleOutput": true,
"httpDownloadConcurrency": 3,
"serve": {
"models": {
"locallens-chat": {
"model": "QWEN3_1_7B_INST_Q4",
"default": true,
"preload": false,
"config": {
"ctx_size": 4096,
"temp": 0.2,
"top_p": 0.9
}
},
"locallens-embed": {
"model": "GTE_LARGE_FP16",
"default": true,
"preload": false
}
}
}
}A few choices worth knowing about:
preload: false— models load on first use, not at boot. Keepsbun run devsnappy.ctx_size: 4096— the chat model's context window. If you raise top-K in retrieval later, keep this in view.temp: 0.2— low temperature for citation-grounded answers. The model should rephrase the source, not invent.
8. Make a place for the source
mkdir src tests examplesEvery walkthrough step from here on writes a file under src/.
tests/ will hold the two Bun tests you'll add for rag.ts and
files.ts. examples/sample-brain/ is the demo folder the CLI
points at.
Where you are now
The project shell is in place: package.json, tsconfig.json,
biome.json, qvac.config.json, and three empty directories (src/,
tests/, examples/). Don't run bun run typecheck yet — with
include: ["src/**/*.ts", "tests/**/*.ts"] and nothing inside either
folder, TypeScript exits with:
error TS18003: No inputs were found in config file 'tsconfig.json'.That's expected at this stage. The first source file lands in the
next step. The moment src/domain.ts exists, bun run typecheck
turns green and stays green for the rest of the walkthrough.
What `bun init` doesn't give you
bun init -y is intentionally minimal. It doesn't add Biome,
QVAC, strict TypeScript flags, or the script set above. Treat
this page as the opinionated layer you bolt on top of bun init.
Everything from here on assumes the project looks like the result
of these eight steps.
Next up: domain types and AppError,
the first real source file.