skills/turso/SKILL.md
Wire Turso (libSQL) into a TanStack Start + Cloudflare Workers app, with native vector search for embeddings, and drive its CLI/API. libSQL is a SQLite fork with built-in vectors (F32_BLOB + vector_top_k + libsql_vector_idx) that upstream SQLite and D1 lack. Covers the critical @libsql/client/web (HTTP, Worker-safe) vs @libsql/client (Node) split, JWT auth, the Drizzle libSQL driver + customType vector column, KNN queries, and when Turso vectors beat Vectorize / pgvector / D1. Use when the user wants Turso, libSQL, vector embeddings / similarity search in SQL, a vector DB, or mentions @libsql/client / TURSO_DATABASE_URL / F32_BLOB.
npx skillsauth add RonanCodes/ronan-skills tursoInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Wire Turso / libSQL into a TanStack Start + Cloudflare Workers app. libSQL is an open-source SQLite fork with native vector search (F32_BLOB, vector_top_k, libsql_vector_idx) that upstream SQLite and Cloudflare D1 do not have. Turso is the managed cloud that hosts it (edge-replicated, JWT-authed, reachable over HTTP).
The headline use case: keep embeddings next to relational rows in one SQLite database, so a single SQL query does the KNN and joins to titles/metadata, no separate vector service to sync against.
Souso note (ADR-0003 / decision #15): smart-cart deliberately chose one D1, no libSQL/Turso, no vector DB (matching is set-maths, and embedded libSQL can't run in a Worker). This skill is a reusable capability; adopting it in Souso contradicts that ADR. The ADR's "can't run in a Worker" point is about the embedded driver,
@libsql/client/webreaches Turso over HTTP and DOES work in a Worker, at the cost of a network hop (vs D1's local binding). Decide with that nuance.
/ro:turso install # CLI + env + Worker-safe client
/ro:turso add-vectors # F32_BLOB column + ANN index + KNN query
/ro:turso add-drizzle # drizzle-orm/libsql + customType vector column
/ro:turso ops # CLI cheat-sheet (create db, tokens, shell)
brew install tursodatabase/tap/turso or curl -sSfL https://get.tur.so/install.sh | bash).~/.claude/.env: TURSO_AUTH_TOKEN (a JWT). The megathon one is a group token (authorises every DB in the group), not a single-DB token, both are JWTs and go in the same slot.Import @libsql/client/web, never @libsql/client.
| Import | Runtime | Transport | Worker? |
| --- | --- | --- | --- |
| @libsql/client | Node | native + WS + HTTP, supports file: | ❌ pulls native deps, won't bundle |
| @libsql/client/web | edge | HTTP only (fetch) | ✅ the Worker import |
The plain import silently works in vite dev/Node then fails to bundle on Workers. Make /web non-negotiable. (Newer forward path: @tursodatabase/serverless — pure fetch, edge-first, with a /compat shim; @libsql/client/web is the battle-tested, Drizzle-proven default this skill uses.)
turso db create smart-cart # create
turso db show smart-cart --url # -> TURSO_DATABASE_URL (libsql://<db>-<org>.turso.io)
turso db tokens create smart-cart # DB-scoped JWT
turso group tokens create <group> # GROUP JWT (all DBs in the group) — what the megathon token is
turso db shell smart-cart # interactive SQL against the remote DB
# .dev.vars (gitignored)
TURSO_DATABASE_URL=libsql://<db>-<org>.turso.io
TURSO_AUTH_TOKEN=<jwt>
wrangler secret put TURSO_DATABASE_URL
wrangler secret put TURSO_AUTH_TOKEN
There is no wrangler.jsonc binding (unlike D1), you connect with URL + token at runtime. Read them off env inside the handler.
src/lib/turso.tspnpm add @libsql/client
import { createClient } from '@libsql/client/web' // /web = HTTP-only, Worker-safe
export function getTurso(env: { TURSO_DATABASE_URL: string; TURSO_AUTH_TOKEN: string }) {
return createClient({ url: env.TURSO_DATABASE_URL, authToken: env.TURSO_AUTH_TOKEN })
}
Construct it inside the request handler with the env binding, not at module top level.
Native vector columns + ANN index + KNN. Verified function names (docs.turso.tech/features/ai-and-embeddings):
F32_BLOB(<dim>) (32-bit float vector, fixed dimensionality).vector32('[...]') converts a JSON-array string to the blob (vector(...) is an alias).CREATE INDEX <idx> ON <tbl>(libsql_vector_idx(<col>, 'metric=cosine')). The libsql_vector_idx() marker is required (it's how libSQL marks an ANN index). Add compress_neighbors=float8 to shrink it.vector_top_k('<idx>', vector32('[...]'), <k>) is a table-valued function returning id (= rowid); join back for columns.vector_distance_cos(a, b) → cosine distance (0..2).CREATE TABLE recipe (
id INTEGER PRIMARY KEY,
title TEXT,
embedding F32_BLOB(1536) -- e.g. OpenAI text-embedding dim
);
CREATE INDEX recipe_idx ON recipe(libsql_vector_idx(embedding, 'metric=cosine'));
INSERT INTO recipe (title, embedding)
VALUES ('Stamppot', vector32('[0.012, -0.044, 0.91, ...]'));
-- 5 nearest neighbours to a query embedding, with distance
SELECT recipe.id, recipe.title,
vector_distance_cos(recipe.embedding, vector32('[...query...]')) AS dist
FROM vector_top_k('recipe_idx', vector32('[...query...]'), 5)
JOIN recipe ON recipe.rowid = id
ORDER BY dist;
Index-syntax warning: the Turso Drizzle docs page shows
... USING vector_cosine(3)— that is stale/incorrect. Uselibsql_vector_idx(col[, 'metric=cosine'])(the canonical engine syntax). Confirm against your libSQL version. ANN is approximate (DiskANN), top-k is approximate NN, not exact.
pnpm add drizzle-orm @libsql/client
import { drizzle } from 'drizzle-orm/libsql'
import { createClient } from '@libsql/client/web' // /web for Workers
const client = createClient({ url: env.TURSO_DATABASE_URL, authToken: env.TURSO_AUTH_TOKEN })
export const db = drizzle(client)
F32_BLOB has no first-class Drizzle column, define a customType:
import { customType } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'
const float32Array = customType<{
data: number[]
config: { dimensions: number }
configRequired: true
driverData: Buffer
}>({
dataType(config) {
return `F32_BLOB(${config.dimensions})`
},
fromDriver(value: Buffer) {
return Array.from(new Float32Array(value.buffer))
},
toDriver(value: number[]) {
return sql`vector32(${JSON.stringify(value)})`
},
})
export const recipe = sqliteTable('recipe', {
id: integer('id').primaryKey(),
title: text('title'),
embedding: float32Array('embedding', { dimensions: 1536 }),
})
vector_top_k is raw SQL (no query builder), and the ANN index goes in a hand-written migration (drizzle-kit won't generate libsql_vector_idx):
const rows = await db
.select({ id: sql`id`, distance: sql`distance` })
.from(sql`vector_top_k('recipe_idx', vector32(${JSON.stringify(queryVec)}), 5)`)
.leftJoin(recipe, sql`${recipe}.id = id`)
(Cosmetic: drizzle-kit push flags customType columns as "changed" each run — drizzle-orm #3047, harmless.)
/web import or it breaks (silently fine in Node, fails to bundle on Workers).*.turso.io per query. Use batch([...]) (one request, implicit all-or-nothing transaction) to fold statements; keep the DB region near the Worker. Avoid interactive transaction() on Workers (holds a write lock ~5s, latency-sensitive).file: DB; Workers have no filesystem). Always remote-over-HTTP.F32_BLOB + vector_top_k out of the box. Choose D1 for the local binding with no vectors; Turso when vectors-in-the-DB is the requirement.TURSO_DATABASE_URL=libsql://<db>-<org>.turso.io
TURSO_AUTH_TOKEN=<jwt> # DB or group token; server-only secret
Server-side only; URL + token are secrets. On Workers always @libsql/client/web. Vector indexes and the customType column go in hand-written migrations, review them.
/ro:neon (Postgres + pgvector), Cloudflare Vectorize, D1. Sibling data skills: /ro:cala, /ro:posthogtesting
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".