Locara

Locara for AI coding agents

This page is written for AI coding assistants generating Locara apps. If you’re a human reading this — fine, but the second person here means “you, the agent.”

Mental model

A Locara app is:

  • A locara.json manifest declaring identity, capabilities, modalities, and models.
  • A React frontend that calls the typed SDK at @locara/sdk.
  • A SQLite schema in db/schema.sql.
  • Capability tests in tests/capabilities.test.ts.

The framework refuses to do anything not declared in the manifest. That’s the contract.

Closed catalogs

Two catalogs are closed enums — do not invent values:

Modalities:

  • text-to-text
  • text-to-embedding
  • speech-to-text
  • text-to-speech
  • voice-to-voice
  • image-to-text
  • text-to-image

Tooling:

  • ocr
  • filesystem.search, filesystem.read, filesystem.write
  • code-exec.python, code-exec.javascript
  • audio.transcribe, audio.synthesize
  • vision.classify
  • browser.fetch
  • vector.search

If the user asks for something outside these, propose the closest fit and explain. Don’t extend the catalog without an ADR.

Capability principles

  1. Default deny. Don’t declare capabilities you don’t need.
  2. Models are pinned by hash. A manifest entry looks like qwen2.5-3b-instruct-q4@sha256:abcdef.... Use the hash from the registry; never invent.
  3. No net unless required. Most Locara apps work fully offline. If you reach for net, justify it.
  4. fs.user-selected is for picker-driven file reads only. It does not grant write access.

SDK rule of thumb

Every SDK call may throw CapabilityDenied. Catch it where the user might benefit from a clearer message; otherwise let it bubble.

import { llm, CapabilityDenied } from '@locara/sdk'

try {
  const r = await llm.chat({ model: 'qwen...', messages })
} catch (e) {
  if (e instanceof CapabilityDenied) showFriendlyError(e)
  else throw e
}

Streaming pattern

import { llm } from '@locara/sdk'

const ac = new AbortController()
for await (const delta of llm.chatStream({ model, messages }, ac.signal)) {
  if (delta.content) appendToken(delta.content)
}

AbortController is the cancellation mechanism. The runtime stops generation cleanly when aborted.

Database pattern

import { db } from '@locara/sdk'

await db.exec({ sql: 'CREATE TABLE messages (id TEXT PRIMARY KEY, content TEXT)' })
await db.exec({ sql: 'INSERT INTO messages VALUES (?, ?)', params: ['x', 'hi'] })
const r = await db.query<{ id: string }>({ sql: 'SELECT id FROM messages' })

The schema is declared in db/schema.sql; locara db migrate applies it.

Templates

Use locara init <template> <name>:

  • blank — minimal text-to-text scaffold.
  • chat — polished chat with history.
  • transcribe — audio-first.
  • docvault — document-first.
  • voice — voice-to-voice.

Pick the closest template and adapt; don’t start from blank if a richer template fits.

Components

Use locara add <component> to copy in:

  • chat, message-bubble, chat-input
  • audio-input, transcript-stream
  • doc-dropzone, doc-preview
  • results-list

Components are copy-in source; you can edit them. Capability requirements are listed in packages/components/registry.json.

Testing

The scaffolded tests/capabilities.test.ts uses @locara/test. Add scenario-specific tests using the same harness:

import { runApp, matchers } from '@locara/test'

const app = await runApp()
await app.simulate('user-interaction')
expect(matchers.toAttemptNoNetwork(app).pass).toBe(true)

When unsure

  • Read /spec for the design intent.
  • Cite spec sections in code comments where useful.
  • Don’t invent fields, modalities, or tools.
  • Run locara verify before declaring done.