Add lastAccessedAt timestamp to nodes with schema migration and backfill. Touch timestamp on read, apply exponential freshness decay (~69-day half-life) to search scoring alongside BM25 and vector weights. Add auto-decay that marks untouched nodes as stale after a configurable threshold, with CLI command and server-side daily interval. Include comprehensive USAGE.md documenting all CLI commands and REST API.
80 lines
2.2 KiB
TypeScript
80 lines
2.2 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
|
|
const SCHEMA = `
|
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
id TEXT PRIMARY KEY,
|
|
kind TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
content TEXT NOT NULL DEFAULT '',
|
|
status TEXT,
|
|
tags TEXT DEFAULT '[]',
|
|
metadata TEXT DEFAULT '{}',
|
|
embedding BLOB,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
is_stale INTEGER DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS edges (
|
|
id TEXT PRIMARY KEY,
|
|
from_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
to_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
type TEXT NOT NULL,
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS node_tags (
|
|
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
tag TEXT NOT NULL,
|
|
PRIMARY KEY (node_id, tag)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);
|
|
CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
|
|
CREATE INDEX IF NOT EXISTS idx_nodes_created ON nodes(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_nodes_stale ON nodes(is_stale);
|
|
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_id);
|
|
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_id);
|
|
CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
|
|
CREATE INDEX IF NOT EXISTS idx_tags_tag ON node_tags(tag);
|
|
`;
|
|
|
|
let _db: Database.Database | null = null;
|
|
|
|
export function getMemoryDir(): string {
|
|
return path.join(process.cwd(), '.memory');
|
|
}
|
|
|
|
export function getDb(): Database.Database {
|
|
if (_db) return _db;
|
|
|
|
const dir = getMemoryDir();
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
_db = new Database(path.join(dir, 'cortex.db'));
|
|
_db.pragma('journal_mode = WAL');
|
|
_db.pragma('foreign_keys = ON');
|
|
_db.exec(SCHEMA);
|
|
|
|
// Migration: add last_accessed_at column
|
|
const cols = _db.prepare("PRAGMA table_info(nodes)").all() as any[];
|
|
if (!cols.some((c: any) => c.name === 'last_accessed_at')) {
|
|
_db.exec('ALTER TABLE nodes ADD COLUMN last_accessed_at INTEGER');
|
|
_db.exec('UPDATE nodes SET last_accessed_at = updated_at WHERE last_accessed_at IS NULL');
|
|
}
|
|
|
|
return _db;
|
|
}
|
|
|
|
export function closeDb(): void {
|
|
if (_db) {
|
|
_db.close();
|
|
_db = null;
|
|
}
|
|
}
|