this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(db): implement SQLite database layer with Drizzle ORM

Add database schema and infrastructure for storing tasks, brain dumps, and subtasks. Uses bun:sqlite for optimal Bun runtime integration and Drizzle ORM for type-safe queries.

Key additions:
- src/db/schema.ts: Items table with hierarchical task support
- src/db/index.ts: Database initialization with auto-migration
- src/db/migrations/: Initial migration and metadata
- src/db/schema.test.ts: Comprehensive database tests
- drizzle.config.ts: Drizzle Kit configuration
- .gitignore: Exclude database files, keep .gitkeep

The items table supports:
- Multiple item types (brain_dump, task, subtask)
- Status tracking (open, in_progress, done, archived)
- Priority levels (0-4, aligned with beads)
- Hierarchical relationships via parentId

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice f324a723 12a646f7

+315 -3
+3 -3
.beads/issues.jsonl
··· 13 13 {"id":"assistant-m8g.1","title":"record_tiny_win tool (src/tools/wins.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:03.571473Z","updated_at":"2025-12-11T13:45:03.571473Z","dependencies":[{"issue_id":"assistant-m8g.1","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:03.571956Z","created_by":"daemon"}]} 14 14 {"id":"assistant-m8g.2","title":"get_wins_summary tool (src/tools/wins.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:04.336122Z","updated_at":"2025-12-11T13:45:04.336122Z","dependencies":[{"issue_id":"assistant-m8g.2","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:04.336616Z","created_by":"daemon"}]} 15 15 {"id":"assistant-m8g.3","title":"Wins database table","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:45:05.347711Z","updated_at":"2025-12-11T13:45:05.347711Z","dependencies":[{"issue_id":"assistant-m8g.3","depends_on_id":"assistant-m8g","type":"parent-child","created_at":"2025-12-11T13:45:05.348177Z","created_by":"daemon"}]} 16 - {"id":"assistant-nno","title":"M2: Tools + Items","description":"","status":"open","priority":0,"issue_type":"epic","created_at":"2025-12-11T13:43:35.019588Z","updated_at":"2025-12-11T13:43:35.019588Z","dependencies":[{"issue_id":"assistant-nno","depends_on_id":"assistant-pqh","type":"blocks","created_at":"2025-12-11T13:43:50.878442Z","created_by":"daemon"}]} 17 - {"id":"assistant-nno.1","title":"Database schema + Drizzle setup (src/db/)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:37.136609Z","updated_at":"2025-12-11T13:44:37.136609Z","dependencies":[{"issue_id":"assistant-nno.1","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:37.137113Z","created_by":"daemon"}]} 18 - {"id":"assistant-nno.2","title":"Tool dispatcher (src/tools/dispatcher.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:38.375255Z","updated_at":"2025-12-11T13:44:38.375255Z","dependencies":[{"issue_id":"assistant-nno.2","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:38.375743Z","created_by":"daemon"}]} 16 + {"id":"assistant-nno","title":"M2: Tools + Items","description":"","status":"in_progress","priority":0,"issue_type":"epic","created_at":"2025-12-11T13:43:35.019588Z","updated_at":"2025-12-11T16:22:59.616868Z","dependencies":[{"issue_id":"assistant-nno","depends_on_id":"assistant-pqh","type":"blocks","created_at":"2025-12-11T13:43:50.878442Z","created_by":"daemon"}]} 17 + {"id":"assistant-nno.1","title":"Database schema + Drizzle setup (src/db/)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:37.136609Z","updated_at":"2025-12-11T16:27:05.155748Z","closed_at":"2025-12-11T16:27:05.155748Z","dependencies":[{"issue_id":"assistant-nno.1","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:37.137113Z","created_by":"daemon"}]} 18 + {"id":"assistant-nno.2","title":"Tool dispatcher (src/tools/dispatcher.ts)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:38.375255Z","updated_at":"2025-12-11T16:27:27.073341Z","closed_at":"2025-12-11T16:27:27.073341Z","dependencies":[{"issue_id":"assistant-nno.2","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:38.375743Z","created_by":"daemon"}]} 19 19 {"id":"assistant-nno.3","title":"parse_brain_dump tool (src/tools/capture.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:39.812103Z","updated_at":"2025-12-11T13:44:39.812103Z","dependencies":[{"issue_id":"assistant-nno.3","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:39.812544Z","created_by":"daemon"}]} 20 20 {"id":"assistant-nno.4","title":"break_down_task tool (src/tools/breakdown.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:40.767448Z","updated_at":"2025-12-11T13:44:40.767448Z","dependencies":[{"issue_id":"assistant-nno.4","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:40.767905Z","created_by":"daemon"}]} 21 21 {"id":"assistant-nno.5","title":"save_item / update_item tools (src/tools/items.ts)","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-11T13:44:41.824017Z","updated_at":"2025-12-11T13:44:41.824017Z","dependencies":[{"issue_id":"assistant-nno.5","depends_on_id":"assistant-nno","type":"parent-child","created_at":"2025-12-11T13:44:41.824488Z","created_by":"daemon"}]}
+4
.gitignore
··· 32 32 33 33 # Finder (MacOS) folder config 34 34 .DS_Store 35 + 36 + # Database files (SQLite) 37 + data/** 38 + !data/.gitkeep
data/.gitkeep

This is a binary file and will not be displayed.

+16
drizzle.config.ts
··· 1 + /** 2 + * Drizzle Kit configuration 3 + * 4 + * Configuration for Drizzle's migration generator and other CLI tools. 5 + */ 6 + 7 + import { defineConfig } from 'drizzle-kit'; 8 + 9 + export default defineConfig({ 10 + schema: './src/db/schema.ts', 11 + out: './src/db/migrations', 12 + dialect: 'sqlite', 13 + dbCredentials: { 14 + url: process.env.DB_PATH || './data/assistant.db', 15 + }, 16 + });
+54
src/db/index.ts
··· 1 + /** 2 + * Database initialization and connection 3 + * 4 + * Sets up SQLite database with Drizzle ORM using bun:sqlite 5 + */ 6 + 7 + import { Database } from 'bun:sqlite'; 8 + import { drizzle } from 'drizzle-orm/bun-sqlite'; 9 + import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; 10 + import { mkdir } from 'node:fs/promises'; 11 + import { dirname } from 'node:path'; 12 + import { config } from '../config'; 13 + import * as schema from './schema'; 14 + 15 + /** 16 + * Initialize the database connection 17 + * 18 + * Creates the parent directory if it doesn't exist. 19 + * Returns a Drizzle database instance configured for bun:sqlite. 20 + */ 21 + async function initDb(): Promise<ReturnType<typeof drizzle<typeof schema>>> { 22 + const dbPath = config.DB_PATH; 23 + 24 + // Create parent directory if it doesn't exist 25 + const dir = dirname(dbPath); 26 + await mkdir(dir, { recursive: true }); 27 + 28 + // Open SQLite connection with bun:sqlite 29 + const sqlite = new Database(dbPath, { create: true }); 30 + 31 + // Enable WAL mode for better concurrent access 32 + sqlite.run('PRAGMA journal_mode = WAL;'); 33 + 34 + // Create Drizzle instance 35 + const db = drizzle(sqlite, { schema }); 36 + 37 + // Run migrations 38 + migrate(db, { migrationsFolder: './src/db/migrations' }); 39 + 40 + return db; 41 + } 42 + 43 + /** 44 + * Database instance 45 + * 46 + * Singleton instance of the Drizzle database. 47 + * Initialized on first import. 48 + */ 49 + export const db = await initDb(); 50 + 51 + /** 52 + * Export schema for use in queries 53 + */ 54 + export { schema };
+11
src/db/migrations/0000_high_zemo.sql
··· 1 + CREATE TABLE `items` ( 2 + `id` text PRIMARY KEY NOT NULL, 3 + `user_id` integer NOT NULL, 4 + `type` text NOT NULL, 5 + `content` text NOT NULL, 6 + `status` text DEFAULT 'open' NOT NULL, 7 + `priority` integer DEFAULT 2 NOT NULL, 8 + `parent_id` text, 9 + `created_at` integer DEFAULT (unixepoch()) NOT NULL, 10 + `updated_at` integer DEFAULT (unixepoch()) NOT NULL 11 + );
+95
src/db/migrations/meta/0000_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "5d1f65f7-f7cd-4508-b4c2-e3675a81d1ed", 5 + "prevId": "00000000-0000-0000-0000-000000000000", 6 + "tables": { 7 + "items": { 8 + "name": "items", 9 + "columns": { 10 + "id": { 11 + "name": "id", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "user_id": { 18 + "name": "user_id", 19 + "type": "integer", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "type": { 25 + "name": "type", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false 30 + }, 31 + "content": { 32 + "name": "content", 33 + "type": "text", 34 + "primaryKey": false, 35 + "notNull": true, 36 + "autoincrement": false 37 + }, 38 + "status": { 39 + "name": "status", 40 + "type": "text", 41 + "primaryKey": false, 42 + "notNull": true, 43 + "autoincrement": false, 44 + "default": "'open'" 45 + }, 46 + "priority": { 47 + "name": "priority", 48 + "type": "integer", 49 + "primaryKey": false, 50 + "notNull": true, 51 + "autoincrement": false, 52 + "default": 2 53 + }, 54 + "parent_id": { 55 + "name": "parent_id", 56 + "type": "text", 57 + "primaryKey": false, 58 + "notNull": false, 59 + "autoincrement": false 60 + }, 61 + "created_at": { 62 + "name": "created_at", 63 + "type": "integer", 64 + "primaryKey": false, 65 + "notNull": true, 66 + "autoincrement": false, 67 + "default": "(unixepoch())" 68 + }, 69 + "updated_at": { 70 + "name": "updated_at", 71 + "type": "integer", 72 + "primaryKey": false, 73 + "notNull": true, 74 + "autoincrement": false, 75 + "default": "(unixepoch())" 76 + } 77 + }, 78 + "indexes": {}, 79 + "foreignKeys": {}, 80 + "compositePrimaryKeys": {}, 81 + "uniqueConstraints": {}, 82 + "checkConstraints": {} 83 + } 84 + }, 85 + "views": {}, 86 + "enums": {}, 87 + "_meta": { 88 + "schemas": {}, 89 + "tables": {}, 90 + "columns": {} 91 + }, 92 + "internal": { 93 + "indexes": {} 94 + } 95 + }
+13
src/db/migrations/meta/_journal.json
··· 1 + { 2 + "version": "7", 3 + "dialect": "sqlite", 4 + "entries": [ 5 + { 6 + "idx": 0, 7 + "version": "6", 8 + "when": 1765470257411, 9 + "tag": "0000_high_zemo", 10 + "breakpoints": true 11 + } 12 + ] 13 + }
+78
src/db/schema.test.ts
··· 1 + /** 2 + * Tests for database schema 3 + */ 4 + 5 + import { describe, expect, test } from 'bun:test'; 6 + import { eq } from 'drizzle-orm'; 7 + import { db, schema } from './index'; 8 + 9 + describe('Database Schema', () => { 10 + test('db instance is defined', () => { 11 + expect(db).toBeDefined(); 12 + }); 13 + 14 + test('schema exports items table', () => { 15 + expect(schema.items).toBeDefined(); 16 + }); 17 + 18 + test('can insert and query items', async () => { 19 + // Insert a test item 20 + const testItem = { 21 + id: `test-${Date.now().toString()}`, 22 + userId: 12345, 23 + type: 'task' as const, 24 + content: 'Test task', 25 + status: 'open' as const, 26 + priority: 2, 27 + }; 28 + 29 + await db.insert(schema.items).values(testItem); 30 + 31 + // Query it back 32 + const result = await db.select().from(schema.items).where(eq(schema.items.id, testItem.id)); 33 + 34 + expect(result).toHaveLength(1); 35 + expect(result[0]?.id).toBe(testItem.id); 36 + expect(result[0]?.userId).toBe(testItem.userId); 37 + expect(result[0]?.type).toBe('task'); 38 + expect(result[0]?.content).toBe('Test task'); 39 + expect(result[0]?.status).toBe('open'); 40 + expect(result[0]?.priority).toBe(2); 41 + 42 + // Clean up 43 + await db.delete(schema.items).where(eq(schema.items.id, testItem.id)); 44 + }); 45 + 46 + test('supports hierarchical tasks with parentId', async () => { 47 + const parentId = `parent-${Date.now().toString()}`; 48 + const childId = `child-${Date.now().toString()}`; 49 + 50 + // Create parent task 51 + await db.insert(schema.items).values({ 52 + id: parentId, 53 + userId: 12345, 54 + type: 'task', 55 + content: 'Parent task', 56 + }); 57 + 58 + // Create child subtask 59 + await db.insert(schema.items).values({ 60 + id: childId, 61 + userId: 12345, 62 + type: 'subtask', 63 + content: 'Child subtask', 64 + parentId: parentId, 65 + }); 66 + 67 + // Query child 68 + const children = await db.select().from(schema.items).where(eq(schema.items.parentId, parentId)); 69 + 70 + expect(children).toHaveLength(1); 71 + expect(children[0]?.id).toBe(childId); 72 + expect(children[0]?.parentId).toBe(parentId); 73 + 74 + // Clean up 75 + await db.delete(schema.items).where(eq(schema.items.id, childId)); 76 + await db.delete(schema.items).where(eq(schema.items.id, parentId)); 77 + }); 78 + });
+41
src/db/schema.ts
··· 1 + /** 2 + * Database schema for ADHD Support Assistant 3 + * 4 + * Defines the SQLite database schema using Drizzle ORM. 5 + */ 6 + 7 + import { sql } from 'drizzle-orm'; 8 + import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 9 + 10 + /** 11 + * Items table - stores tasks, brain dumps, and subtasks 12 + * 13 + * Supports hierarchical task breakdown with parent-child relationships 14 + */ 15 + export const items = sqliteTable('items', { 16 + id: text('id').primaryKey().notNull(), 17 + userId: integer('user_id').notNull(), // Telegram user ID 18 + type: text('type', { enum: ['brain_dump', 'task', 'subtask'] }).notNull(), 19 + content: text('content').notNull(), 20 + status: text('status', { enum: ['open', 'in_progress', 'done', 'archived'] }) 21 + .notNull() 22 + .default('open'), 23 + priority: integer('priority').notNull().default(2), // 0-4, like beads 24 + parentId: text('parent_id'), // nullable, for subtasks 25 + createdAt: integer('created_at', { mode: 'timestamp' }) 26 + .notNull() 27 + .default(sql`(unixepoch())`), 28 + updatedAt: integer('updated_at', { mode: 'timestamp' }) 29 + .notNull() 30 + .default(sql`(unixepoch())`), 31 + }); 32 + 33 + /** 34 + * Type for inserting new items 35 + */ 36 + export type InsertItem = typeof items.$inferInsert; 37 + 38 + /** 39 + * Type for selecting items from the database 40 + */ 41 + export type SelectItem = typeof items.$inferSelect;