Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"typescript": "^5"
},
"dependencies": {
"@hasna/cloud": "^0.1.41",
"@hasna/events": "^0.1.6",
"@modelcontextprotocol/sdk": "^1.26.0",
"chalk": "^5.3.0",
Expand Down
14 changes: 7 additions & 7 deletions src/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/**
* SQLite DB module for hooks — persistent storage at ~/.hasna/hooks/hooks.db
*
* Uses the @hasna/cloud SQLite adapter with WAL mode for concurrent reads.
* Uses bun:sqlite with WAL mode for concurrent reads.
* Supports HASNA_HOOKS_DATA_DIR / HOOKS_DATA_DIR and HASNA_HOOKS_DB_PATH / HOOKS_DB_PATH env overrides.
*/

import { SqliteAdapter } from "@hasna/cloud";
import { Database } from "bun:sqlite";
import { existsSync, mkdirSync, cpSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { runMigrations } from "./migrations";
import { runLegacyImport } from "./legacy-import";
import { runRetention } from "./retention";

let instance: SqliteAdapter | null = null;
let instance: Database | null = null;

function resolveDataDir(): string {
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
Expand Down Expand Up @@ -46,14 +46,14 @@ function ensureDir(dbPath: string): void {
}
}

export function getDb(): SqliteAdapter {
export function getDb(): Database {
if (instance) return instance;

const dbPath = getDbPath();
const isNew = dbPath === ":memory:" || !existsSync(dbPath);
ensureDir(dbPath);

instance = new SqliteAdapter(dbPath);
instance = new Database(dbPath);
instance.exec("PRAGMA journal_mode=WAL");
instance.exec("PRAGMA foreign_keys=ON");
runMigrations(instance);
Expand Down Expand Up @@ -82,8 +82,8 @@ export function closeDb(): void {
}
}

export function createTestDb(): SqliteAdapter {
const db = new SqliteAdapter(":memory:");
export function createTestDb(): Database {
const db = new Database(":memory:");
db.exec("PRAGMA journal_mode=WAL");
db.exec("PRAGMA foreign_keys=ON");
return db;
Expand Down
50 changes: 27 additions & 23 deletions src/db/legacy-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
* Tracks completion via a `_meta` table row keyed "legacy_import_done".
*/

import type { DbAdapter } from "@hasna/cloud";
import type { Database } from "bun:sqlite";
import { existsSync, readdirSync, readFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";

const META_KEY = "legacy_import_done";

function ensureMetaTable(db: DbAdapter): void {
function ensureMetaTable(db: Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS _meta (
key TEXT PRIMARY KEY,
Expand All @@ -24,21 +24,21 @@ function ensureMetaTable(db: DbAdapter): void {
`);
}

function isAlreadyDone(db: DbAdapter): boolean {
function isAlreadyDone(db: Database): boolean {
ensureMetaTable(db);
const row = db.get("SELECT value FROM _meta WHERE key = ?", META_KEY) as { value: string } | undefined;
const row = db.query<{ value: string }, [string]>("SELECT value FROM _meta WHERE key = ?").get(META_KEY);
return row?.value === "1";
}

function markDone(db: DbAdapter): void {
db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", META_KEY, "1");
function markDone(db: Database): void {
db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [META_KEY, "1"]);
}

function nanoid(): string {
return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
}

function importJsonlFile(db: DbAdapter, filePath: string): number {
function importJsonlFile(db: Database, filePath: string): number {
let count = 0;
try {
const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
Expand All @@ -49,14 +49,16 @@ function importJsonlFile(db: DbAdapter, filePath: string): number {
`INSERT OR IGNORE INTO hook_events
(id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
nanoid(),
entry.timestamp ?? new Date().toISOString(),
entry.session_id ?? "legacy",
"sessionlog",
"PostToolUse",
entry.tool_name ?? null,
entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
null,
[
nanoid(),
entry.timestamp ?? new Date().toISOString(),
entry.session_id ?? "legacy",
"sessionlog",
"PostToolUse",
entry.tool_name ?? null,
entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
null,
]
);
count++;
} catch {
Expand All @@ -69,7 +71,7 @@ function importJsonlFile(db: DbAdapter, filePath: string): number {
return count;
}

function importErrorsLog(db: DbAdapter, filePath: string): number {
function importErrorsLog(db: Database, filePath: string): number {
let count = 0;
try {
const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
Expand All @@ -85,12 +87,14 @@ function importErrorsLog(db: DbAdapter, filePath: string): number {
`INSERT OR IGNORE INTO hook_events
(id, timestamp, session_id, hook_name, event_type, error)
VALUES (?, ?, ?, ?, ?, ?)`,
nanoid(),
timestamp,
sessionPrefix ? `legacy-${sessionPrefix}` : "legacy",
"errornotify",
"PostToolUse",
errorMsg.slice(0, 500),
[
nanoid(),
timestamp,
sessionPrefix ? `legacy-${sessionPrefix}` : "legacy",
"errornotify",
"PostToolUse",
errorMsg.slice(0, 500),
]
);
count++;
} catch {
Expand All @@ -103,7 +107,7 @@ function importErrorsLog(db: DbAdapter, filePath: string): number {
return count;
}

export function runLegacyImport(db: DbAdapter): void {
export function runLegacyImport(db: Database): void {
try {
if (isAlreadyDone(db)) return;

Expand Down
4 changes: 2 additions & 2 deletions src/db/migrations/001_initial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
* Creates hook_events table and indexes.
*/

import type { DbAdapter } from "@hasna/cloud";
import type { Database } from "bun:sqlite";
import { CREATE_HOOK_EVENTS_TABLE, CREATE_INDEXES } from "../schema";

export function up(db: DbAdapter): void {
export function up(db: Database): void {
db.exec(CREATE_HOOK_EVENTS_TABLE);
for (const idx of CREATE_INDEXES) {
db.exec(idx);
Expand Down
13 changes: 7 additions & 6 deletions src/db/migrations/002_session_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
* (fresh databases created from the updated schema.ts).
*/

import type { DbAdapter } from "@hasna/cloud";
import type { Database } from "bun:sqlite";
import { CREATE_HOOK_EVENTS_TABLE, CREATE_INDEXES } from "../schema";

export function up(db: DbAdapter): void {
const row = db.get(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
"hook_events",
) as { sql: string | null } | undefined;
export function up(db: Database): void {
const row = db
.query<{ sql: string | null }, [string]>(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?"
)
.get("hook_events");

// Table missing (shouldn't happen — 001 creates it) or already current.
if (!row?.sql) return;
Expand Down
13 changes: 7 additions & 6 deletions src/db/migrations/003_user_prompt_submit_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
* already current (fresh databases or databases rebuilt by a newer 002).
*/

import type { DbAdapter } from "@hasna/cloud";
import type { Database } from "bun:sqlite";
import { CREATE_HOOK_EVENTS_TABLE, CREATE_INDEXES } from "../schema";

export function up(db: DbAdapter): void {
const row = db.get(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
"hook_events",
) as { sql: string | null } | undefined;
export function up(db: Database): void {
const row = db
.query<{ sql: string | null }, [string]>(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?"
)
.get("hook_events");

if (!row?.sql) return;
if (row.sql.includes("UserPromptSubmit")) return;
Expand Down
17 changes: 8 additions & 9 deletions src/db/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
* Migrations are additive-only, never destructive.
*/

import type { DbAdapter } from "@hasna/cloud";
import type { Database } from "bun:sqlite";
import { up as migration001 } from "./001_initial";
import { up as migration002 } from "./002_session_events";
import { up as migration003 } from "./003_user_prompt_submit_event";

interface Migration {
version: string;
up: (db: DbAdapter) => void;
up: (db: Database) => void;
}

const MIGRATIONS: Migration[] = [
Expand All @@ -20,7 +20,7 @@ const MIGRATIONS: Migration[] = [
{ version: "003_user_prompt_submit_event", up: migration003 },
];

function ensureMigrationsTable(db: DbAdapter): void {
function ensureMigrationsTable(db: Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
Expand All @@ -29,23 +29,22 @@ function ensureMigrationsTable(db: DbAdapter): void {
`);
}

function getApplied(db: DbAdapter): Set<string> {
const rows = db.all("SELECT version FROM schema_migrations") as Array<{ version: string }>;
function getApplied(db: Database): Set<string> {
const rows = db.query<{ version: string }, []>("SELECT version FROM schema_migrations").all();
return new Set(rows.map((r) => r.version));
}

export function runMigrations(db: DbAdapter): void {
export function runMigrations(db: Database): void {
ensureMigrationsTable(db);
const applied = getApplied(db);

for (const migration of MIGRATIONS) {
if (applied.has(migration.version)) continue;

migration.up(db);
db.run(
"INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)",
db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [
migration.version,
new Date().toISOString(),
);
]);
}
}
Loading
Loading