LocalLens

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 few node: modules)

No OpenAI key. No Pinecone account. No GPU. Everything runs locally through QVAC.

1. Create the project

mkdir locallens
cd locallens
bun init -y

bun 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/biome

What each one is for:

PackageWhy
@qvac/sdkThe local AI loop: model loading, RAG, completion. The only runtime dependency LocalLens has.
typescriptStrict types for the eight files you're about to write.
@types/bunType definitions for Bun.argv, Bun.serve, Bun.file, import.meta.dir.
@biomejs/biomeOne 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:

package.json
{
  "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:

package.json
{
  "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:

tsconfig.json
{
  "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: true lets you write import "./domain.ts" in source files. Bun runs them directly; the explicit extension keeps tsc happy.
  • verbatimModuleSyntax: true requires import type for 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:

biome.json
{
  "$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:

qvac.config.json
{
  "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. Keeps bun run dev snappy.
  • 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 examples

Every 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.

On this page