Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ Grok CLI (`@vibe-kit/grok-cli`) is a single-package TypeScript CLI tool — no d
### Environment

- **Bun** must be installed (not pre-installed on Cloud VMs). The update script handles this.
- `GROK_API_KEY` environment variable is required for API calls. Set it as a secret.
- `GROK_API_KEY` environment variable is required for API calls. Set it as a secret.
- `src/storage/db.ts` runtime-loads `bun:sqlite` and falls back to `node:sqlite` when unavailable, so the storage layer also works under Node. Note `bun run test` (`bunx vitest run`) still executes under the Bun runtime; to actually exercise the Node fallback path, run `node node_modules/.bin/vitest run`.

## Maintaining this file

Keep this file for knowledge useful to almost every future agent session in this project.
Do not repeat what the codebase already shows; point to the authoritative file or command instead.
Prefer rewriting or pruning existing entries over appending new ones.
When updating this file, preserve this bar for all agents and keep entries concise.
1 change: 1 addition & 0 deletions CLAUDE.md
65 changes: 65 additions & 0 deletions src/storage/db.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import fs from "fs";
import os from "os";
import path from "path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { closeDatabase, getDatabase, withTransaction } from "./db";

const originalHome = process.env.HOME;

describe("storage database", () => {
const tempRoot = path.join(process.cwd(), ".tmp-db-tests");
let tempHome = "";

beforeEach(() => {
fs.mkdirSync(tempRoot, { recursive: true });
tempHome = fs.mkdtempSync(path.join(tempRoot, "grok-db-home-"));
process.env.HOME = tempHome;
vi.spyOn(os, "homedir").mockReturnValue(tempHome);
closeDatabase();
});

afterEach(() => {
closeDatabase();
vi.restoreAllMocks();
process.env.HOME = originalHome;
fs.rmSync(tempRoot, { recursive: true, force: true });
});

it("initializes and round-trips a write/read", () => {
const db = getDatabase();

db.exec(`
CREATE TABLE IF NOT EXISTS storage_smoke_test (
id INTEGER PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
`);

db.prepare("INSERT INTO storage_smoke_test (id, value) VALUES (?, ?)").run(1, "hello world");

const row = db.prepare("SELECT value FROM storage_smoke_test WHERE id = ?").get(1) as {
value: string;
};

expect(row.value).toBe("hello world");
});

it("commits writes made inside a transaction", () => {
getDatabase().exec(`
CREATE TABLE IF NOT EXISTS storage_smoke_test (
id INTEGER PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
`);

withTransaction((db) => {
db.prepare("INSERT INTO storage_smoke_test (id, value) VALUES (?, ?)").run(2, "in transaction");
});

const row = getDatabase().prepare("SELECT value FROM storage_smoke_test WHERE id = ?").get(2) as {
value: string;
};

expect(row.value).toBe("in transaction");
});
});
80 changes: 78 additions & 2 deletions src/storage/db.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Database } from "bun:sqlite";
import type { DatabaseSync } from "node:sqlite";
import fs from "fs";
import { createRequire } from "module";
import os from "os";
import path from "path";
import { applyMigrations } from "./migrations";

const require = createRequire(import.meta.url);

export interface SQLiteStatement {
run(...params: unknown[]): unknown;
get(...params: unknown[]): unknown;
Expand Down Expand Up @@ -53,7 +56,10 @@ class BunSqliteDatabase implements SQLiteDatabase {
private readonly db: Database;

constructor(filename: string) {
this.db = new Database(filename, { create: true, strict: true });
const BunDatabase = loadBunDatabase();
this.db = BunDatabase
? new BunDatabase(filename, { create: true, strict: true })
: new NodeSqliteDatabase(filename);
}

exec(sql: string): void {
Expand Down Expand Up @@ -89,6 +95,76 @@ class BunSqliteDatabase implements SQLiteDatabase {
}
}

type Database = {
exec(sql: string): void;
run(sql: string, params?: unknown): unknown;
query(sql: string): { get(params?: unknown): unknown; all(params?: unknown): unknown[] };
transaction<T>(fn: () => T): () => T;
close(): void;
};

function loadBunDatabase(): (new (filename: string, options: { create: boolean; strict: boolean }) => Database) | null {
try {
return require("bun:sqlite").Database as new (
filename: string,
options: { create: boolean; strict: boolean },
) => Database;
} catch {
return null;
}
}

class NodeSqliteDatabase implements Database {
private readonly db: DatabaseSync;

constructor(filename: string) {
const { DatabaseSync } = require("node:sqlite") as typeof import("node:sqlite");
this.db = new DatabaseSync(filename);
}

exec(sql: string): void {
this.db.exec(sql);
}

run(sql: string, params?: unknown): unknown {
const statement = this.db.prepare(sql);
if (params === undefined) return statement.run();
return Array.isArray(params) ? statement.run(...(params as never[])) : statement.run(params as never);
}

query(sql: string): { get(params?: unknown): unknown; all(params?: unknown): unknown[] } {
const statement = this.db.prepare(sql);
return {
get: (params?: unknown) => {
if (params === undefined) return statement.get();
return Array.isArray(params) ? statement.get(...(params as never[])) : statement.get(params as never);
},
all: (params?: unknown) => {
if (params === undefined) return statement.all();
return Array.isArray(params) ? statement.all(...(params as never[])) : statement.all(params as never);
},
};
Comment thread
cursor[bot] marked this conversation as resolved.
}

transaction<T>(fn: () => T): () => T {
return () => {
this.db.exec("BEGIN");
try {
const result = fn();
this.db.exec("COMMIT");
return result;
} catch (error) {
this.db.exec("ROLLBACK");
throw error;
}
};
}

close(): void {
this.db.close();
}
}

function normalizeBinding(params: unknown[]): unknown {
if (params.length === 0) return undefined;
return params.length === 1 ? params[0] : params;
Expand Down