diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts new file mode 100644 index 00000000..319dbb7b --- /dev/null +++ b/packages/db/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/schema/index.ts", + out: "./src/migrations", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 00000000..7f1b1580 --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,27 @@ +{ + "name": "@paperclip/db", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "generate": "drizzle-kit generate", + "migrate": "tsx src/migrate.ts", + "seed": "tsx src/seed.ts" + }, + "dependencies": { + "@paperclip/shared": "workspace:*", + "drizzle-orm": "^0.38.4", + "postgres": "^3.4.5" + }, + "devDependencies": { + "drizzle-kit": "^0.30.4", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vitest": "^3.0.5" + } +} diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts new file mode 100644 index 00000000..6bfe8cbb --- /dev/null +++ b/packages/db/src/client.ts @@ -0,0 +1,10 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema/index.js"; + +export function createDb(url: string) { + const sql = postgres(url); + return drizzle(sql, { schema }); +} + +export type Db = ReturnType; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts new file mode 100644 index 00000000..90a6463f --- /dev/null +++ b/packages/db/src/index.ts @@ -0,0 +1,2 @@ +export { createDb, type Db } from "./client.js"; +export * from "./schema/index.js"; diff --git a/packages/db/src/migrate.ts b/packages/db/src/migrate.ts new file mode 100644 index 00000000..146d5b2b --- /dev/null +++ b/packages/db/src/migrate.ts @@ -0,0 +1,13 @@ +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; +import { drizzle } from "drizzle-orm/postgres-js"; + +const url = process.env.DATABASE_URL; +if (!url) throw new Error("DATABASE_URL is required"); + +const sql = postgres(url, { max: 1 }); +const db = drizzle(sql); + +await migrate(db, { migrationsFolder: new URL("./migrations", import.meta.url).pathname }); +await sql.end(); +console.log("Migrations complete"); diff --git a/packages/db/src/schema/activity_log.ts b/packages/db/src/schema/activity_log.ts new file mode 100644 index 00000000..4db9e42e --- /dev/null +++ b/packages/db/src/schema/activity_log.ts @@ -0,0 +1,12 @@ +import { pgTable, uuid, text, timestamp, jsonb } from "drizzle-orm/pg-core"; +import { agents } from "./agents.js"; + +export const activityLog = pgTable("activity_log", { + id: uuid("id").primaryKey().defaultRandom(), + action: text("action").notNull(), + entityType: text("entity_type").notNull(), + entityId: uuid("entity_id").notNull(), + agentId: uuid("agent_id").references(() => agents.id), + details: jsonb("details"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/packages/db/src/schema/agents.ts b/packages/db/src/schema/agents.ts new file mode 100644 index 00000000..52e79a7a --- /dev/null +++ b/packages/db/src/schema/agents.ts @@ -0,0 +1,15 @@ +import { type AnyPgColumn, pgTable, uuid, text, integer, timestamp, jsonb } from "drizzle-orm/pg-core"; + +export const agents = pgTable("agents", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull(), + role: text("role").notNull().default("general"), + status: text("status").notNull().default("idle"), + budgetCents: integer("budget_cents").notNull().default(0), + spentCents: integer("spent_cents").notNull().default(0), + lastHeartbeat: timestamp("last_heartbeat", { withTimezone: true }), + reportsTo: uuid("reports_to").references((): AnyPgColumn => agents.id), + metadata: jsonb("metadata"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/packages/db/src/schema/goals.ts b/packages/db/src/schema/goals.ts new file mode 100644 index 00000000..5eeeb55e --- /dev/null +++ b/packages/db/src/schema/goals.ts @@ -0,0 +1,13 @@ +import { type AnyPgColumn, pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; +import { agents } from "./agents.js"; + +export const goals = pgTable("goals", { + id: uuid("id").primaryKey().defaultRandom(), + title: text("title").notNull(), + description: text("description"), + level: text("level").notNull().default("task"), + parentId: uuid("parent_id").references((): AnyPgColumn => goals.id), + ownerId: uuid("owner_id").references(() => agents.id), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts new file mode 100644 index 00000000..2862bf22 --- /dev/null +++ b/packages/db/src/schema/index.ts @@ -0,0 +1,5 @@ +export { agents } from "./agents.js"; +export { projects } from "./projects.js"; +export { goals } from "./goals.js"; +export { issues } from "./issues.js"; +export { activityLog } from "./activity_log.js"; diff --git a/packages/db/src/schema/issues.ts b/packages/db/src/schema/issues.ts new file mode 100644 index 00000000..4e0edd5a --- /dev/null +++ b/packages/db/src/schema/issues.ts @@ -0,0 +1,17 @@ +import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; +import { agents } from "./agents.js"; +import { projects } from "./projects.js"; +import { goals } from "./goals.js"; + +export const issues = pgTable("issues", { + id: uuid("id").primaryKey().defaultRandom(), + title: text("title").notNull(), + description: text("description"), + status: text("status").notNull().default("backlog"), + priority: text("priority").notNull().default("medium"), + projectId: uuid("project_id").references(() => projects.id), + assigneeId: uuid("assignee_id").references(() => agents.id), + goalId: uuid("goal_id").references(() => goals.id), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/packages/db/src/schema/projects.ts b/packages/db/src/schema/projects.ts new file mode 100644 index 00000000..defc6ac7 --- /dev/null +++ b/packages/db/src/schema/projects.ts @@ -0,0 +1,9 @@ +import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; + +export const projects = pgTable("projects", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull(), + description: text("description"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts new file mode 100644 index 00000000..3d02cefb --- /dev/null +++ b/packages/db/src/seed.ts @@ -0,0 +1,66 @@ +import { eq, inArray } from "drizzle-orm"; +import { createDb } from "./client.js"; +import { agents, projects, issues, goals } from "./schema/index.js"; + +const url = process.env.DATABASE_URL; +if (!url) throw new Error("DATABASE_URL is required"); + +const db = createDb(url); + +console.log("Seeding database..."); + +const [ceo, engineer, researcher] = await db + .insert(agents) + .values([ + { name: "CEO Agent", role: "ceo", status: "active" }, + { name: "Engineer Agent", role: "engineer", status: "idle" }, + { name: "Researcher Agent", role: "researcher", status: "idle" }, + ]) + .returning(); + +// Wire up reporting hierarchy: engineer and researcher report to CEO +await db + .update(agents) + .set({ reportsTo: ceo!.id }) + .where(inArray(agents.id, [engineer!.id, researcher!.id])); + +const [project] = await db + .insert(projects) + .values([{ name: "Paperclip MVP", description: "Build the initial paperclip management platform" }]) + .returning(); + +const [goal] = await db + .insert(goals) + .values([ + { + title: "Launch MVP", + description: "Ship the minimum viable product", + level: "milestone", + ownerId: ceo!.id, + }, + ]) + .returning(); + +await db.insert(issues).values([ + { + title: "Set up database schema", + description: "Create initial Drizzle schema with all core tables", + status: "done", + priority: "high", + projectId: project!.id, + assigneeId: engineer!.id, + goalId: goal!.id, + }, + { + title: "Implement agent heartbeat", + description: "Add periodic heartbeat mechanism for agent health monitoring", + status: "in_progress", + priority: "medium", + projectId: project!.id, + assigneeId: engineer!.id, + goalId: goal!.id, + }, +]); + +console.log("Seed complete"); +process.exit(0); diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 00000000..a086b149 --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/db/vitest.config.ts b/packages/db/vitest.config.ts new file mode 100644 index 00000000..f624398e --- /dev/null +++ b/packages/db/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +});