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
154 lines (117 loc) • 6.88 kB
Markdown
# Runtime Boundary Design — AKM 0.9.0
> **Implementation note (added later):** both target files below now exist —
> `src/storage/database.ts` and `src/runtime.ts` — so this design has landed.
> The "3 hard-import files" named in the original checklist (`src/indexer/db.ts`,
> `src/core/state-db.ts`, `src/workflows/db.ts`) no longer describes the current
> tree: `src/indexer/db.ts` was later split into `src/storage/repositories/index-*.ts`
> (its `Database` opener lives in `src/storage/repositories/index-connection.ts`,
> which imports `openDatabase`/`Database` from `src/storage/database.ts` as this
> design specifies). `src/workflows/db.ts` did exist historically but has since
> been removed — workflow run persistence now lives in
> `src/storage/repositories/workflow-runs-repository.ts`.
> `src/core/state-db.ts` still exists and also imports from `src/storage/database.ts`.
> The checklist items below are kept as the historical design record.
## Problem
Runtime-specific dependencies (`bun:sqlite`, `Bun.*` APIs) leak directly into 33+ source files.
This creates unnecessary coupling, makes Node.js compatibility hard, and means every future
runtime change has a large blast radius.
## Goal
Two files own the entire runtime boundary. Everything else is clean application code.
---
## File 1: `src/storage/database.ts`
Single source of truth for SQLite. The rest of the app never imports from `bun:sqlite` or
`better-sqlite3` directly — only from here.
**Responsibilities:**
- Detect runtime (bun vs node) once at module load
- Load the appropriate SQLite driver (`bun:sqlite` on Bun, `better-sqlite3` on Node)
- Export `Database` type — the common structural type both drivers share
- Export `openDatabase(path, opts?)` factory
**Design constraints:**
- No adapter classes, no interface hierarchies — the driver APIs are already nearly identical
- The exported `Database` type should be the structural intersection of what both drivers expose
- `openDatabase()` should be a simple function, not a factory class
- `db.query()` (Bun-specific) must be normalised — replace with `db.prepare().all()` at this boundary
**Architect: investigate and fill in:**
- [ ] Read `src/indexer/db.ts`, `src/core/state-db.ts`, `src/workflows/db.ts` — the 3 hard-import files
- [ ] Identify every method AKM actually calls on a `Database` instance (exec, prepare, transaction, close, query, etc.)
- [ ] Check whether `better-sqlite3`'s type definitions can be used as the exported `Database` type directly, or if a small structural type alias is needed
- [ ] Check `sqliteVec.load(db)` — confirm whether it accepts both driver handles or needs a raw handle escape hatch
- [ ] Write the complete file
---
## File 2: `src/runtime.ts`
~60 lines. Named exports for every `Bun.*` API called outside of the SQLite layer.
Each export works on both runtimes.
**Known Bun.* call sites to cover (architect: verify these are complete):**
| Export name | Bun API | Node equivalent |
|---|---|---|
| `spawnSync(cmd, opts)` | `Bun.spawnSync` | `child_process.spawnSync` |
| `spawn(cmd, opts)` | `Bun.spawn` | `child_process.spawn` |
| `readStdin(limitBytes)` | `Bun.stdin.stream()` | `for await (chunk of process.stdin)` |
| `writeResponseToFile(path, res)` | `Bun.write(path, response)` | `stream/promises pipeline` |
| `sha256Hex(data)` | `new Bun.CryptoHasher('sha256')` | `node:crypto createHash` |
| `md5Hex(data)` | `new Bun.CryptoHasher('md5')` | `node:crypto createHash` |
| `semverOrder(a, b)` | `Bun.semver.order` | `semver.compare` (add dep) |
| `getDirname(importMetaUrl)` | `import.meta.dir` | `path.dirname(new URL(url).pathname)` |
| `resolveModule(spec, from)` | `Bun.resolveSync` | `require.resolve` (already has fallback) |
| `sleepSync(ms)` | `Bun.sleepSync` | `Atomics.wait` (already has fallback) |
| `mainPath` | `Bun.main` | `process.argv[1]` |
**Design constraints:**
- Runtime detection: `const isBun = !!process.versions?.bun` at top of file, used inline
- No class, no factory — just named function exports
- Each export branches once on `isBun`, no per-call overhead
- `node:crypto` is available in both Bun and Node — the crypto exports can skip the `isBun`
branch entirely and just use `createHash` always (simpler, same perf for a CLI)
**Architect: investigate and fill in:**
- [ ] `grep -rn "Bun\." src --include="*.ts"` — confirm the table above is complete
- [ ] `grep -rn "import\.meta\.dir" src --include="*.ts"` — list all sites, note which have fallbacks
- [ ] Confirm `Readable.toWeb()` is the right Node 18 approach for `writeResponseToFile`
- [ ] Write the complete file
---
## Call Site Changes
Once both files exist, the remaining work is mechanical:
**SQLite type-only imports (~30 files):**
```ts
// Before
import type { Database } from 'bun:sqlite'
// After
import type { Database } from '../storage/database'
```
**SQLite hard imports (3 files — `indexer/db.ts`, `core/state-db.ts`, `workflows/db.ts`):**
- Replace `import { Database } from 'bun:sqlite'` with `import { openDatabase, type Database } from '../storage/database'`
- Replace `new Database(path)` with `openDatabase(path)`
- Replace `db.query<T>(sql).all(params)` with `db.prepare(sql).all(params)` (Bun-specific API)
- Remove `SQLQueryBindings` spread casts
**Bun.* call sites (~10 files):**
```ts
// Before
Bun.spawnSync(['git', 'ls-files', ...])
// After
import { spawnSync } from '../runtime'
spawnSync(['git', 'ls-files', ...])
```
**Architect: investigate and fill in:**
- [ ] List every call site file and the exact import line change needed
- [ ] Flag any call site where the shim return shape differs from the Bun API (e.g. stdout as string vs Buffer)
---
## `package.json` Changes
- Add `better-sqlite3` as `optionalDependency` (prebuilt binaries, no compile step on common platforms)
- Add `@types/better-sqlite3` to `devDependencies`
- Add `semver` to `dependencies` (tiny, zero-deps)
- Add `node: ">=22"` to `engines` (raised from 20.12 when Node 20 support was dropped, 2026-07) (`@clack/core` uses `node:util.styleText`,
which was added in Node 20.12)
- Require Node >= 22 in the npm `preinstall` guard
---
## What This Is NOT
- Not an adapter pattern with interface hierarchies
- Not a ports-and-adapters architecture
- Not a plugin system
- Not a DI container
Just two modules that own the runtime details so the rest of the codebase doesn't have to.
---
## Child Issues to Create (after design is complete)
1. `src/storage/database.ts` + migrate 3 hard-import files
2. Mechanical: update 30 type-only imports (sed pass + tsc verify)
3. `src/runtime.ts` + update ~10 call sites
4. `package.json` + relax preinstall guard + CI Node matrix
Plus: audit open GitHub issues on itlackey/akm and close any that are superseded by this work
or otherwise stale (milestone 0.9.0 items already addressed, etc.).