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

239 lines (238 loc) • 12 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/. /** * SQLite runtime boundary. * * Single source of truth for opening SQLite database handles. The rest of the * codebase imports the {@link Database} type and {@link openDatabase} factory * from here and NEVER imports `bun:sqlite` or `better-sqlite3` directly. * * Runtime selection: * - On Bun (the primary/test runtime) we use the built-in `bun:sqlite`. * - On Node.js (CI-tested by the node-smoke job's smoke + compat suites) we * use `better-sqlite3`, loaded via a runtime-gated dynamic `require` so the Bun path never * imports it (it is an optionalDependency and may be uninstalled or * uncompiled when running under Bun). * * Both driver handles are structurally compatible across the small surface AKM * uses (`prepare`, `exec`, `run`, `transaction`, `close` on the handle; * `get`, `all`, `iterate`, `run` on prepared statements). The Bun-specific `db.query()` * helper is normalised away — callers use `db.prepare(sql).all(...)` instead. * * This file is intentionally NOT an adapter/DI/ports-and-adapters layer. It is * a plain module: a structural type plus a factory function. The handle it * returns is the real underlying driver instance (so e.g. `sqlite-vec`'s * `load(db)`, used to drop the retired `entries_vec` table, receives the * genuine driver handle and works unchanged). * * @module storage/database */ import { createRequire } from "node:module"; // Detect the runtime exactly once at module load. const isBun = !!process.versions?.bun; // A CommonJS-style require usable from this ESM module on both runtimes. Used // to load the runtime-specific driver lazily so that neither `bun:sqlite` (a // Bun built-in, unresolvable on Node) nor `better-sqlite3` (an optional native // dep, possibly absent under Bun) is statically imported. const nodeRequire = createRequire(import.meta.url); /** * Open a SQLite database handle at `path` on the current runtime's driver * (`bun:sqlite` on Bun, `better-sqlite3` on Node). Returns a handle conforming * to the structural {@link Database} type. */ export function openDatabase(path, opts) { return isBun ? openBunDatabase(path, opts) : openNodeDatabase(path, opts); } /** * {@link openDatabase} with a finalize-on-close guard (issue #720). * * bun:sqlite's `Database.close()` is a `sqlite3_close_v2`: with any UNFINALIZED * `prepare()` statement outstanding, the underlying connection — and its WAL * shared-memory mapping — survives as a zombie until GC finalizes the * statements. A later connection in the same process then cannot leave WAL * mode (`PRAGMA journal_mode = DELETE` → "database is locked"), which is * exactly what silently defeated the migrate-apply single-file conversion. * (`db.query()` statements are Database-cached and immune; `prepare()` is the * dominant idiom in the migration helpers.) * * This variant tracks `prepare()` results and finalizes them (idempotently) * before the real `close()`, so close always means CLOSED. It is deliberately * OPT-IN for the migrate-apply flow's short-lived helper connections and the * migration test fixtures — NOT the global default: long-lived/hot paths cache * prepared statements per connection (e.g. the entries upsert WeakMap) and * worker flows may still be stepping a statement when a sibling close lands; * force-finalizing under them changes behavior they were built on. */ export function openDatabaseFinalizing(path, opts) { const db = openDatabase(path, opts); const tracked = new Set(); const origPrepare = db.prepare.bind(db); const origClose = db.close.bind(db); db.prepare = ((sql, ...rest) => { const stmt = origPrepare(sql, ...rest); tracked.add(stmt); return stmt; }); db.close = ((...args) => { for (const stmt of tracked) { try { stmt.finalize?.(); } catch { // Already finalized (double-finalize is the idempotent no-op case). } } tracked.clear(); return origClose(...args); }); return db; } function openBunDatabase(path, opts) { const { Database: BunDatabase } = loadBunSqlite(); // Only pass an options object when an option is actually set. bun:sqlite // raises SQLITE_MISUSE if handed an options bag with all-undefined fields, // and every current caller opens with just a path — so the no-opts path must // remain byte-identical to the original `new Database(path)`. const db = opts ? new BunDatabase(path, bunOptions(opts)) : new BunDatabase(path); return db; } function bunOptions(opts) { const out = {}; if (opts.readonly !== undefined) out.readonly = opts.readonly; if (opts.create !== undefined) out.create = opts.create; // bun:sqlite reads a partial options object as the full set of open flags: // with neither `readonly` nor `readwrite` set the handle carries no access // mode and every statement fails with SQLITE_MISUSE ("bad parameter or // other API misuse"). Default to writable, matching the driver's no-options // open and better-sqlite3's semantics. if (!out.readonly) out.readwrite = true; return out; } let bunSqliteModule; function loadBunSqlite() { // `bun:sqlite` is a Bun built-in. This function is only ever called when // `isBun` is true, so Node never resolves the `bun:` specifier. Loaded via // require (not a static import) to keep Node's ESM resolver from choking on // the `bun:` specifier when this module is merely imported under Node. if (!bunSqliteModule) { bunSqliteModule = nodeRequire("bun:sqlite"); } return bunSqliteModule; } let betterSqlite3Ctor; /** * The binding is absent or unbuildable — a toolchain/install problem. */ const MISSING_BINDING_REMEDY = "akm could not load 'better-sqlite3', the SQLite driver it needs on Node.js.\n" + " • Reinstall akm with a working C/C++ build toolchain so its optional\n" + " 'better-sqlite3' native binding builds (a global `npm i -g better-sqlite3`\n" + " will NOT be resolved — Node loads it from akm's own node_modules).\n" + " • Or run akm under Bun, which has a built-in SQLite driver and needs no native build."; /** * Recognize a binding built for a DIFFERENT Node ABI than the one now running, * and answer with the one command that fixes it. * * This is the most likely failure a real user hits, and it is not a broken * install: a native addon is compiled (or a prebuilt binary is selected) for * the Node major present at `npm install` time. Upgrade Node afterwards and the * same file no longer loads. * * It is matched rather than described because the symptom text varies and the * previous wording only named ONE of them. The prebuilt-binary path — which is * now the normal path, since better-sqlite3 is pinned to a version publishing a * prebuild for every supported Node (see package.json → pinNotes) — reports * `Module did not self-register`, saying nothing about versions at all. A * from-source build reports the explicit `NODE_MODULE_VERSION` mismatch. Asking * the user to decide which bullet applies is the step worth deleting. */ export function abiMismatchRemedy(message) { const ABI_MISMATCH_SHAPES = [ "did not self-register", // prebuilt binary for another ABI "NODE_MODULE_VERSION", // explicit mismatch, from-source build "was compiled against a different", // same, older phrasing "invalid ELF header", // binary for another platform/arch entirely ]; if (!ABI_MISMATCH_SHAPES.some((shape) => message.includes(shape))) return undefined; return ("akm could not load 'better-sqlite3': its native binding was built for a different\n" + `Node.js version than the one now running (this Node is ABI ${process.versions.modules}).\n` + "This is what happens when Node is upgraded after akm is installed. It is NOT a\n" + "broken install, and reinstalling akm is not required.\n" + " Fix: npm rebuild better-sqlite3 # in akm's install directory\n" + " Or: npm install -g akm-cli # reinstall, rebuilding against this Node\n" + " Or: run akm under Bun, whose built-in SQLite driver needs no native binding."); } function loadBetterSqlite3() { if (!betterSqlite3Ctor) { // Runtime-gated dynamic require: only reached when NOT on Bun, so Bun never // resolves or loads the optional `better-sqlite3` native dependency. // `better-sqlite3` is an optionalDependency, so `npm i` can succeed without // it (or with a native build that failed). Convert the raw MODULE_NOT_FOUND // into an actionable message instead of a cryptic onboarding crash. let mod; try { mod = nodeRequire("better-sqlite3"); } catch (err) { // An ABI mismatch does NOT arrive here — `require` succeeds and the // failure lands at construction (see openNodeDatabase). This path is a // genuinely absent or unresolvable module. `abiMismatchRemedy` is still // consulted because a from-source build CAN fail at load with the // explicit NODE_MODULE_VERSION message. const raw = err instanceof Error ? err.message : String(err); throw new Error(`${abiMismatchRemedy(raw) ?? MISSING_BINDING_REMEDY}\n Underlying load error: ${raw}`); } betterSqlite3Ctor = mod.default ?? mod; } return betterSqlite3Ctor; } function openNodeDatabase(path, opts) { const BetterSqlite3 = loadBetterSqlite3(); // better-sqlite3 validates option *values* strictly and throws // `Expected the "readonly" option to be a boolean` if the key is present with // an `undefined` value — so only include each option when it is actually set, // matching the no-opts byte-identical path on the Bun side. const options = {}; if (opts?.readonly !== undefined) options.readonly = opts.readonly; if (opts?.create === false) options.fileMustExist = true; // Construction, not `require`, is where an ABI mismatch surfaces. // `require("better-sqlite3")` SUCCEEDS against a binding built for another // Node ABI — the package resolves its `.node` file lazily — so the loader's // catch never sees this error and cannot explain it. Verified against a real // ABI-127 binding under Node 24 (ABI 137): `require()` returned a function // and `new Database(...)` threw. Wrapping the require alone left the most // likely real-world failure reported as a bare Node internals message. let db; try { db = opts ? new BetterSqlite3(path, options) : new BetterSqlite3(path); } catch (err) { const raw = err instanceof Error ? err.message : String(err); const remedy = abiMismatchRemedy(raw); if (!remedy) throw err; throw new Error(`${remedy}\n Underlying error: ${raw}`); } return { prepare: db.prepare.bind(db), exec: db.exec.bind(db), // better-sqlite3 exposes mutations on prepared statements, while // bun:sqlite also provides db.run(). Normalize the latter at the provider // boundary so callers and maintenance wrappers can rely on one contract. run: (sql, ...params) => db.prepare(sql).run(...params), // sqlite-vec's load(db) calls db.loadExtension(); without forwarding it the // extension could never load on Node. loadExtension: db.loadExtension.bind(db), transaction: db.transaction.bind(db), get inTransaction() { return db.inTransaction; }, close: db.close.bind(db), }; }