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.jsonmanifest 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-texttext-to-embeddingspeech-to-texttext-to-speechvoice-to-voiceimage-to-texttext-to-image
Tooling:
ocrfilesystem.search,filesystem.read,filesystem.writecode-exec.python,code-exec.javascriptaudio.transcribe,audio.synthesizevision.classifybrowser.fetchvector.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
- Default deny. Don’t declare capabilities you don’t need.
- 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. - No
netunless required. Most Locara apps work fully offline. If you reach fornet, justify it. fs.user-selectedis 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-inputaudio-input,transcript-streamdoc-dropzone,doc-previewresults-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
/specfor the design intent. - Cite spec sections in code comments where useful.
- Don’t invent fields, modalities, or tools.
- Run
locara verifybefore declaring done.