UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

146 lines (141 loc) • 6.53 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * The derived `entries` layout: DDL plus the layout marker. * * `index_meta.version` is a layout marker. An older layout is never refused: * readers serve what is there, and the writable opener (`ensureSchema`, * `index-schema.ts`) brings it up to date in place — added and dropped * columns, retired tables dropped, a one-time full-text rebuild — without * touching embeddings, utility scores, or the LLM enrichment cache (the one * exception is the LLM entity graph, unconditionally dropped since its * retirement in 0.9.17-alpha.9). A newer layout is refused, naming the * upgrade. */ // 26: declared links (#935) live in `asset_links`, one row per link, owned by // the entry that declares it. The writable opener derives them in place from // each entry's stored `document_json` (`migrateToDeclaredLinks`, // index-schema.ts); releases before 26 did not store them. // 25: vectors live only in `embeddings`; the sqlite-vec mirror `entries_vec` // is dropped (`dropVecMirror`, index-schema.ts), and so is the fragment FTS // table `entry_fragments_fts`, which search no longer reads. `entries` keeps // a hash of each entry's embedding input (`embed_hash`) instead of the text // (`search_text`); the text is derived from `document_json` when embedding. // 24: the FTS5 tables are contentless (`content=''`) — the indexed text lives // once, in `entries` / `entry_fragments`, and FTS rows are keyed by rowid only. // 23 and earlier stored a second copy of every indexed field in FTS5's own // content shadow tables. `ensureFtsLayout` (index-schema.ts) rebuilds the FTS // tables from the stored entries when it finds the older layout. export const CANONICAL_INDEX_DB_VERSION = 26; export const CANONICAL_ENTRY_SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_ref TEXT NOT NULL UNIQUE, bundle_id TEXT NOT NULL, component_id TEXT NOT NULL, concept_id TEXT NOT NULL, adapter_id TEXT NOT NULL, type TEXT NOT NULL, file_path TEXT NOT NULL, content_hash TEXT, document_json TEXT NOT NULL, derived_from TEXT, embed_hash TEXT ); CREATE INDEX IF NOT EXISTS idx_entries_bundle ON entries(bundle_id); CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(type); CREATE INDEX IF NOT EXISTS idx_entries_file_path ON entries(file_path); CREATE INDEX IF NOT EXISTS idx_entries_derived_from ON entries(derived_from); CREATE TABLE IF NOT EXISTS entry_fragments ( entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE, safe_markdown TEXT NOT NULL ); -- Declared links (#935). dst_bundle NULL: the declaring entry's own bundle. CREATE TABLE IF NOT EXISTS asset_links ( entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE, ord INTEGER NOT NULL, kind TEXT NOT NULL, raw TEXT NOT NULL, dst_bundle TEXT, dst_concept TEXT NOT NULL, PRIMARY KEY (entry_id, ord) ) WITHOUT ROWID; CREATE INDEX IF NOT EXISTS idx_asset_links_dst ON asset_links(dst_concept); `; // The FTS table is contentless: FTS5 keeps only the inverted index, and a row // is addressed by its rowid (`entries_fts.rowid = entries.id`, see // index-fts-repository.ts). `contentless_delete=1` lets a row be deleted by // rowid alone — an external-content table would instead need the exact text // originally indexed, which is derived in JS from `document_json` // (`buildSearchFields`) and would corrupt the index the first time that // derivation changed. The UNINDEXED columns store nothing and read back NULL; // they stay declared so the insert statements and the readers' // `COALESCE(f.entry_id, f.rowid)` joins are valid against both this layout // and the content-bearing one older releases wrote — which is also the // layout written when the linked SQLite predates `contentless_delete` (3.43, // e.g. the macOS 13 system library Bun links there). const CONTENTLESS_OPTIONS = "content='', contentless_delete=1,"; export function entriesFtsDdl(contentless) { return ` CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5( entry_id UNINDEXED, name, description, tags, hints, content, ${contentless ? CONTENTLESS_OPTIONS : ""} tokenize='porter unicode61' );`; } /** Whether the linked SQLite's FTS5 supports `contentless_delete` (3.43.0+). */ export function supportsContentlessDelete(db) { const version = db.prepare("SELECT sqlite_version() AS version").get().version; const [major = 0, minor = 0] = version.split(".").map(Number); return major > 3 || (major === 3 && minor >= 43); } function sqlString(value) { return `'${value.replaceAll("'", "''")}'`; } export function readTableSql(db, name) { const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${sqlString(name)}`).get(); return row?.sql ?? null; } export function tableExists(db, name) { return readTableSql(db, name) !== null; } /** * Columns this release's readers need in `entries` (layout 21+). The writable * opener adds `embed_hash` to an older table, so it is not required here. */ const REQUIRED_ENTRY_COLUMNS = [ "id", "item_ref", "bundle_id", "component_id", "concept_id", "adapter_id", "type", "file_path", "content_hash", "document_json", "derived_from", ]; /** Required `entries` columns the table lacks; every column when there is no `entries` table. */ export function missingEntryColumns(db) { const present = new Set(db.prepare("PRAGMA table_info(entries)").all().map((c) => c.name)); return REQUIRED_ENTRY_COLUMNS.filter((column) => !present.has(column)); } /** * Whether an index database has an `entries` table this release can read. A * fresh or empty file has none; an index older than layout 21 has one keyed * by columns this release no longer reads, and serves nothing until the next * `akm index` recreates it. */ export function hasCurrentEntriesTable(db) { try { return missingEntryColumns(db).length === 0; } catch { return false; } } /** True when an FTS5 table's DDL declares the contentless layout this release writes. */ export function isContentlessFtsDdl(sql) { return sql !== null && /content\s*=\s*''/.test(sql); }