cognova 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.env.example +58 -0
  2. package/Claude/CLAUDE.md +92 -0
  3. package/Claude/hooks/lib/__init__.py +1 -0
  4. package/Claude/hooks/lib/hook_client.py +207 -0
  5. package/Claude/hooks/log-event.py +97 -0
  6. package/Claude/hooks/pre-compact.py +46 -0
  7. package/Claude/hooks/session-end.py +26 -0
  8. package/Claude/hooks/session-start.py +35 -0
  9. package/Claude/hooks/stop-extract.py +40 -0
  10. package/Claude/rules/frontmatter.md +54 -0
  11. package/Claude/rules/markdown.md +43 -0
  12. package/Claude/rules/note-organization.md +33 -0
  13. package/Claude/settings.json +54 -0
  14. package/Claude/skills/README.md +136 -0
  15. package/Claude/skills/_lib/__init__.py +1 -0
  16. package/Claude/skills/_lib/api.py +164 -0
  17. package/Claude/skills/_lib/output.py +95 -0
  18. package/Claude/skills/environment/SKILL.md +73 -0
  19. package/Claude/skills/environment/environment.py +239 -0
  20. package/Claude/skills/memory/SKILL.md +153 -0
  21. package/Claude/skills/memory/memory.py +270 -0
  22. package/Claude/skills/project/SKILL.md +105 -0
  23. package/Claude/skills/project/project.py +203 -0
  24. package/Claude/skills/skill-creator/SKILL.md +261 -0
  25. package/Claude/skills/task/SKILL.md +135 -0
  26. package/Claude/skills/task/task.py +310 -0
  27. package/LICENSE +21 -0
  28. package/README.md +176 -0
  29. package/app/app.config.ts +8 -0
  30. package/app/app.vue +39 -0
  31. package/app/assets/css/main.css +10 -0
  32. package/app/components/AppLogo.vue +40 -0
  33. package/app/components/AssistantPanel.client.vue +518 -0
  34. package/app/components/ConfirmModal.vue +84 -0
  35. package/app/components/TemplateMenu.vue +49 -0
  36. package/app/components/agents/AgentActivityChart.client.vue +105 -0
  37. package/app/components/agents/AgentActivityChart.server.vue +25 -0
  38. package/app/components/agents/AgentForm.vue +304 -0
  39. package/app/components/agents/AgentRunModal.vue +154 -0
  40. package/app/components/agents/AgentStatsCards.vue +98 -0
  41. package/app/components/chat/ChatInput.vue +85 -0
  42. package/app/components/chat/ConversationList.vue +78 -0
  43. package/app/components/chat/MessageBubble.vue +81 -0
  44. package/app/components/chat/StreamingMessage.vue +36 -0
  45. package/app/components/chat/ToolCallBlock.vue +77 -0
  46. package/app/components/editor/CodeEditor.client.vue +212 -0
  47. package/app/components/editor/CodeEditorFallback.vue +12 -0
  48. package/app/components/editor/DocumentEditor.vue +326 -0
  49. package/app/components/editor/DocumentMetadata.vue +140 -0
  50. package/app/components/editor/MarkdownEditor.vue +146 -0
  51. package/app/components/files/FileTree.vue +436 -0
  52. package/app/components/hooks/HookActivityChart.client.vue +117 -0
  53. package/app/components/hooks/HookActivityChart.server.vue +25 -0
  54. package/app/components/hooks/HookStatsCards.vue +63 -0
  55. package/app/components/hooks/RecentEventsTable.vue +123 -0
  56. package/app/components/hooks/ToolBreakdownTable.vue +72 -0
  57. package/app/components/search/DashboardSearch.vue +122 -0
  58. package/app/components/tasks/ProjectSelect.vue +35 -0
  59. package/app/components/tasks/TaskCard.vue +182 -0
  60. package/app/components/tasks/TaskDetail.vue +160 -0
  61. package/app/components/tasks/TaskForm.vue +280 -0
  62. package/app/components/tasks/TaskList.vue +69 -0
  63. package/app/components/view/ViewToc.vue +85 -0
  64. package/app/composables/useAgents.ts +153 -0
  65. package/app/composables/useAuth.ts +73 -0
  66. package/app/composables/useChat.ts +298 -0
  67. package/app/composables/useDocument.ts +141 -0
  68. package/app/composables/useEditor.ts +100 -0
  69. package/app/composables/useFileTree.ts +220 -0
  70. package/app/composables/useHookEvents.ts +68 -0
  71. package/app/composables/useMemories.ts +83 -0
  72. package/app/composables/useNotificationBus.ts +154 -0
  73. package/app/composables/usePreferences.ts +131 -0
  74. package/app/composables/useProjects.ts +97 -0
  75. package/app/composables/useSearch.ts +52 -0
  76. package/app/composables/useTasks.ts +201 -0
  77. package/app/composables/useTerminal.ts +135 -0
  78. package/app/layouts/auth.vue +20 -0
  79. package/app/layouts/dashboard.vue +186 -0
  80. package/app/layouts/view.vue +60 -0
  81. package/app/middleware/auth.ts +9 -0
  82. package/app/pages/agents/[id].vue +602 -0
  83. package/app/pages/agents/index.vue +412 -0
  84. package/app/pages/chat.vue +146 -0
  85. package/app/pages/dashboard.vue +80 -0
  86. package/app/pages/docs.vue +131 -0
  87. package/app/pages/hooks.vue +163 -0
  88. package/app/pages/index.vue +249 -0
  89. package/app/pages/login.vue +60 -0
  90. package/app/pages/memories.vue +282 -0
  91. package/app/pages/settings.vue +625 -0
  92. package/app/pages/tasks.vue +312 -0
  93. package/app/pages/view/[uuid].vue +376 -0
  94. package/dist/cli/index.js +2711 -0
  95. package/drizzle.config.ts +10 -0
  96. package/nuxt.config.ts +98 -0
  97. package/package.json +107 -0
  98. package/server/api/agents/[id]/cancel.post.ts +27 -0
  99. package/server/api/agents/[id]/run.post.ts +34 -0
  100. package/server/api/agents/[id]/runs.get.ts +45 -0
  101. package/server/api/agents/[id]/stats.get.ts +94 -0
  102. package/server/api/agents/[id].delete.ts +29 -0
  103. package/server/api/agents/[id].get.ts +25 -0
  104. package/server/api/agents/[id].patch.ts +55 -0
  105. package/server/api/agents/index.get.ts +15 -0
  106. package/server/api/agents/index.post.ts +48 -0
  107. package/server/api/agents/stats.get.ts +86 -0
  108. package/server/api/auth/[...all].ts +5 -0
  109. package/server/api/conversations/[id].delete.ts +16 -0
  110. package/server/api/conversations/[id].get.ts +34 -0
  111. package/server/api/conversations/index.get.ts +17 -0
  112. package/server/api/documents/[id]/index.delete.ts +47 -0
  113. package/server/api/documents/[id]/index.put.ts +102 -0
  114. package/server/api/documents/[id]/public.get.ts +60 -0
  115. package/server/api/documents/[id]/restore.post.ts +65 -0
  116. package/server/api/documents/by-path.post.ts +168 -0
  117. package/server/api/documents/index.get.ts +48 -0
  118. package/server/api/fs/delete.post.ts +41 -0
  119. package/server/api/fs/list.get.ts +99 -0
  120. package/server/api/fs/mkdir.post.ts +44 -0
  121. package/server/api/fs/move.post.ts +68 -0
  122. package/server/api/fs/read.post.ts +48 -0
  123. package/server/api/fs/rename.post.ts +55 -0
  124. package/server/api/fs/write.post.ts +51 -0
  125. package/server/api/health.get.ts +40 -0
  126. package/server/api/home.get.ts +26 -0
  127. package/server/api/hooks/events/index.get.ts +56 -0
  128. package/server/api/hooks/events/index.post.ts +36 -0
  129. package/server/api/hooks/stats.get.ts +99 -0
  130. package/server/api/memory/[id].delete.ts +26 -0
  131. package/server/api/memory/context.get.ts +83 -0
  132. package/server/api/memory/extract.post.ts +42 -0
  133. package/server/api/memory/search.get.ts +70 -0
  134. package/server/api/memory/store.post.ts +31 -0
  135. package/server/api/projects/[id]/index.delete.ts +40 -0
  136. package/server/api/projects/[id]/index.get.ts +25 -0
  137. package/server/api/projects/[id]/index.put.ts +50 -0
  138. package/server/api/projects/index.get.ts +20 -0
  139. package/server/api/projects/index.post.ts +34 -0
  140. package/server/api/secrets/[key].delete.ts +31 -0
  141. package/server/api/secrets/[key].get.ts +30 -0
  142. package/server/api/secrets/[key].put.ts +52 -0
  143. package/server/api/secrets/index.get.ts +20 -0
  144. package/server/api/secrets/index.post.ts +58 -0
  145. package/server/api/tasks/[id]/index.delete.ts +46 -0
  146. package/server/api/tasks/[id]/index.get.ts +24 -0
  147. package/server/api/tasks/[id]/index.put.ts +70 -0
  148. package/server/api/tasks/[id]/restore.post.ts +49 -0
  149. package/server/api/tasks/index.get.ts +53 -0
  150. package/server/api/tasks/index.post.ts +47 -0
  151. package/server/api/tasks/tags.get.ts +21 -0
  152. package/server/api/user/email.patch.ts +56 -0
  153. package/server/db/index.ts +76 -0
  154. package/server/db/migrate.ts +41 -0
  155. package/server/db/schema.ts +345 -0
  156. package/server/db/seed.ts +46 -0
  157. package/server/db/types.ts +28 -0
  158. package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
  159. package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
  160. package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
  161. package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
  162. package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
  163. package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
  164. package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
  165. package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
  166. package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
  167. package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
  168. package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
  169. package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
  170. package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
  171. package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
  172. package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
  173. package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
  174. package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
  175. package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
  176. package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
  177. package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
  178. package/server/drizzle/migrations/meta/_journal.json +76 -0
  179. package/server/middleware/auth.ts +79 -0
  180. package/server/plugins/00.env-validate.ts +38 -0
  181. package/server/plugins/01.api-token.ts +31 -0
  182. package/server/plugins/02.database.ts +54 -0
  183. package/server/plugins/03.file-watcher.ts +65 -0
  184. package/server/plugins/04.cron-agents.ts +26 -0
  185. package/server/routes/_ws/chat.ts +252 -0
  186. package/server/routes/notifications.ts +47 -0
  187. package/server/routes/terminal.ts +98 -0
  188. package/server/services/agent-executor.ts +218 -0
  189. package/server/services/cron-scheduler.ts +78 -0
  190. package/server/services/memory-extractor.ts +120 -0
  191. package/server/utils/agent-cleanup.ts +91 -0
  192. package/server/utils/agent-registry.ts +95 -0
  193. package/server/utils/auth.ts +33 -0
  194. package/server/utils/chat-session-manager.ts +59 -0
  195. package/server/utils/crypto.ts +40 -0
  196. package/server/utils/db-guard.ts +12 -0
  197. package/server/utils/db-state.ts +63 -0
  198. package/server/utils/document-sync.ts +207 -0
  199. package/server/utils/frontmatter.ts +84 -0
  200. package/server/utils/notification-bus.ts +60 -0
  201. package/server/utils/path-validator.ts +55 -0
  202. package/server/utils/pty-manager.ts +130 -0
  203. package/shared/types/index.ts +604 -0
  204. package/shared/utils/language-detection.ts +87 -0
  205. package/tsconfig.json +10 -0
@@ -0,0 +1,70 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+ import type { UpdateTaskInput } from '~~/shared/types'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const id = getRouterParam(event, 'id')
10
+
11
+ if (!id)
12
+ throw createError({ statusCode: 400, message: 'Task ID is required' })
13
+
14
+ const body = await readBody<UpdateTaskInput>(event)
15
+
16
+ // Validate priority if provided
17
+ if (body.priority !== undefined && (body.priority < 1 || body.priority > 3))
18
+ throw createError({ statusCode: 400, message: 'Priority must be 1 (Low), 2 (Medium), or 3 (High)' })
19
+
20
+ const db = getDb()
21
+
22
+ // Check task exists
23
+ const [existing] = await db
24
+ .select({ id: schema.tasks.id, status: schema.tasks.status })
25
+ .from(schema.tasks)
26
+ .where(eq(schema.tasks.id, id))
27
+ .limit(1)
28
+
29
+ if (!existing)
30
+ throw createError({ statusCode: 404, message: 'Task not found' })
31
+
32
+ const userId = event.context.user?.id
33
+
34
+ const updates: Record<string, unknown> = {
35
+ modifiedAt: new Date(),
36
+ modifiedBy: userId
37
+ }
38
+
39
+ if (body.title !== undefined) updates.title = body.title.trim()
40
+ if (body.description !== undefined) updates.description = body.description?.trim() || null
41
+ if (body.priority !== undefined) updates.priority = body.priority
42
+ if (body.projectId !== undefined) updates.projectId = body.projectId || null
43
+ if (body.tags !== undefined) updates.tags = body.tags
44
+ if (body.dueDate !== undefined) updates.dueDate = body.dueDate ? new Date(body.dueDate) : null
45
+
46
+ // Handle status change
47
+ if (body.status !== undefined) {
48
+ updates.status = body.status
49
+
50
+ // Auto-set completedAt when marking as done
51
+ if (body.status === 'done' && existing.status !== 'done')
52
+ updates.completedAt = new Date()
53
+ else if (body.status !== 'done' && existing.status === 'done')
54
+ updates.completedAt = null
55
+ }
56
+
57
+ await db
58
+ .update(schema.tasks)
59
+ .set(updates)
60
+ .where(eq(schema.tasks.id, id))
61
+
62
+ // Fetch updated task with project relation
63
+ const [task] = await db.query.tasks.findMany({
64
+ where: (tasks, { eq }) => eq(tasks.id, id),
65
+ with: { project: true },
66
+ limit: 1
67
+ })
68
+
69
+ return { data: task }
70
+ })
@@ -0,0 +1,49 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ requireDb(event)
7
+
8
+ const id = getRouterParam(event, 'id')
9
+
10
+ if (!id)
11
+ throw createError({ statusCode: 400, message: 'Task ID is required' })
12
+
13
+ const db = getDb()
14
+
15
+ // Check task exists
16
+ const [existing] = await db
17
+ .select({ id: schema.tasks.id, deletedAt: schema.tasks.deletedAt })
18
+ .from(schema.tasks)
19
+ .where(eq(schema.tasks.id, id))
20
+ .limit(1)
21
+
22
+ if (!existing)
23
+ throw createError({ statusCode: 404, message: 'Task not found' })
24
+
25
+ if (!existing.deletedAt)
26
+ throw createError({ statusCode: 400, message: 'Task is not deleted' })
27
+
28
+ const userId = event.context.user?.id
29
+
30
+ // Restore
31
+ await db
32
+ .update(schema.tasks)
33
+ .set({
34
+ deletedAt: null,
35
+ deletedBy: null,
36
+ modifiedAt: new Date(),
37
+ modifiedBy: userId
38
+ })
39
+ .where(eq(schema.tasks.id, id))
40
+
41
+ // Fetch updated task with project relation
42
+ const [task] = await db.query.tasks.findMany({
43
+ where: (tasks, { eq }) => eq(tasks.id, id),
44
+ with: { project: true },
45
+ limit: 1
46
+ })
47
+
48
+ return { data: task }
49
+ })
@@ -0,0 +1,53 @@
1
+ import { eq, isNull, ilike, inArray, and, or } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ requireDb(event)
7
+
8
+ const query = getQuery(event)
9
+ const includeDeleted = query.includeDeleted === 'true'
10
+ const status = query.status as string | string[] | undefined
11
+ const projectId = query.projectId as string | undefined
12
+ const search = query.search as string | undefined
13
+
14
+ const db = getDb()
15
+
16
+ const conditions = []
17
+
18
+ // Soft delete filter
19
+ if (!includeDeleted)
20
+ conditions.push(isNull(schema.tasks.deletedAt))
21
+
22
+ // Status filter (can be single or array)
23
+ if (status) {
24
+ const statuses = (Array.isArray(status) ? status : [status]) as ('todo' | 'in_progress' | 'done' | 'blocked')[]
25
+ conditions.push(inArray(schema.tasks.status, statuses))
26
+ }
27
+
28
+ // Project filter
29
+ if (projectId)
30
+ conditions.push(eq(schema.tasks.projectId, projectId))
31
+
32
+ // Search filter (title and description)
33
+ if (search) {
34
+ const searchPattern = `%${search}%`
35
+ conditions.push(
36
+ or(
37
+ ilike(schema.tasks.title, searchPattern),
38
+ ilike(schema.tasks.description, searchPattern)
39
+ )
40
+ )
41
+ }
42
+
43
+ const tasks = await db.query.tasks.findMany({
44
+ where: conditions.length > 0 ? and(...conditions) : undefined,
45
+ with: {
46
+ project: true,
47
+ creator: true
48
+ },
49
+ orderBy: (tasks, { desc }) => [desc(tasks.createdAt)]
50
+ })
51
+
52
+ return { data: tasks }
53
+ })
@@ -0,0 +1,47 @@
1
+ import { getDb, schema } from '~~/server/db'
2
+ import { requireDb } from '~~/server/utils/db-guard'
3
+ import type { CreateTaskInput } from '~~/shared/types'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ requireDb(event)
7
+
8
+ const body = await readBody<CreateTaskInput>(event)
9
+
10
+ if (!body.title?.trim())
11
+ throw createError({ statusCode: 400, message: 'Task title is required' })
12
+
13
+ // Validate priority if provided
14
+ if (body.priority !== undefined && (body.priority < 1 || body.priority > 3))
15
+ throw createError({ statusCode: 400, message: 'Priority must be 1 (Low), 2 (Medium), or 3 (High)' })
16
+
17
+ const db = getDb()
18
+
19
+ const userId = event.context.user?.id
20
+
21
+ const result = await db
22
+ .insert(schema.tasks)
23
+ .values({
24
+ title: body.title.trim(),
25
+ description: body.description?.trim() || null,
26
+ status: body.status || 'todo',
27
+ priority: body.priority ?? 2,
28
+ projectId: body.projectId || null,
29
+ dueDate: body.dueDate ? new Date(body.dueDate) : null,
30
+ tags: body.tags || [],
31
+ createdBy: userId
32
+ })
33
+ .returning()
34
+
35
+ const task = result[0]
36
+ if (!task)
37
+ throw createError({ statusCode: 500, message: 'Failed to create task' })
38
+
39
+ // Fetch with project relation
40
+ const [taskWithProject] = await db.query.tasks.findMany({
41
+ where: (tasks, { eq }) => eq(tasks.id, task.id),
42
+ with: { project: true },
43
+ limit: 1
44
+ })
45
+
46
+ return { data: taskWithProject }
47
+ })
@@ -0,0 +1,21 @@
1
+ import { isNull } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ requireDb(event)
7
+
8
+ const db = getDb()
9
+
10
+ // Get all unique tags from non-deleted tasks
11
+ const result = await db
12
+ .select({ tags: schema.tasks.tags })
13
+ .from(schema.tasks)
14
+ .where(isNull(schema.tasks.deletedAt))
15
+
16
+ // Flatten and deduplicate tags
17
+ const allTags = result.flatMap(r => r.tags || [])
18
+ const uniqueTags = [...new Set(allTags)].sort()
19
+
20
+ return { data: uniqueTags }
21
+ })
@@ -0,0 +1,56 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb, schema } from '../../db'
3
+ import { auth } from '../../utils/auth'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const session = await auth.api.getSession({ headers: event.headers })
7
+
8
+ if (!session?.user) {
9
+ throw createError({
10
+ statusCode: 401,
11
+ message: 'Unauthorized'
12
+ })
13
+ }
14
+
15
+ const body = await readBody<{ email: string }>(event)
16
+
17
+ if (!body.email) {
18
+ throw createError({
19
+ statusCode: 400,
20
+ message: 'Email is required'
21
+ })
22
+ }
23
+
24
+ // Basic email validation
25
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
26
+ if (!emailRegex.test(body.email)) {
27
+ throw createError({
28
+ statusCode: 400,
29
+ message: 'Invalid email format'
30
+ })
31
+ }
32
+
33
+ const db = getDb()
34
+
35
+ // Check if email is already taken by another user
36
+ const existingUser = await db
37
+ .select()
38
+ .from(schema.user)
39
+ .where(eq(schema.user.email, body.email))
40
+ .limit(1)
41
+
42
+ if (existingUser.length > 0 && existingUser[0]!.id !== session.user.id) {
43
+ throw createError({
44
+ statusCode: 409,
45
+ message: 'Email is already in use'
46
+ })
47
+ }
48
+
49
+ // Update the user's email directly
50
+ await db
51
+ .update(schema.user)
52
+ .set({ email: body.email })
53
+ .where(eq(schema.user.id, session.user.id))
54
+
55
+ return { success: true, email: body.email }
56
+ })
@@ -0,0 +1,76 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js'
2
+ import postgres from 'postgres'
3
+ import { sql } from 'drizzle-orm'
4
+ import * as schema from './schema'
5
+ import { setDbState } from '~~/server/utils/db-state'
6
+
7
+ interface DbConfig {
8
+ ssl: boolean | 'require' | 'prefer'
9
+ max: number
10
+ idleTimeout: number
11
+ connectTimeout: number
12
+ }
13
+
14
+ function getDbConfig(): DbConfig {
15
+ const url = process.env.DATABASE_URL || ''
16
+ const isNeon = url.includes('neon.tech')
17
+ const isLocal = url.includes('localhost') || url.includes('db:5432') || url.includes('127.0.0.1')
18
+
19
+ return {
20
+ ssl: isNeon ? 'require' : false,
21
+ max: isLocal ? 10 : 5,
22
+ idleTimeout: isNeon ? 20 : 0,
23
+ connectTimeout: isNeon ? 10 : 5
24
+ }
25
+ }
26
+
27
+ let queryClient: ReturnType<typeof postgres> | null = null
28
+ let db: ReturnType<typeof drizzle<typeof schema>> | null = null
29
+
30
+ export function getDb() {
31
+ if (!db) {
32
+ const connectionString = process.env.DATABASE_URL
33
+ if (!connectionString)
34
+ throw new Error('DATABASE_URL not configured')
35
+
36
+ const config = getDbConfig()
37
+ queryClient = postgres(connectionString, {
38
+ ssl: config.ssl,
39
+ max: config.max,
40
+ idle_timeout: config.idleTimeout,
41
+ connect_timeout: config.connectTimeout
42
+ })
43
+ db = drizzle(queryClient, { schema })
44
+ }
45
+ return db
46
+ }
47
+
48
+ export function getQueryClient() {
49
+ if (!queryClient)
50
+ getDb()
51
+ return queryClient!
52
+ }
53
+
54
+ export async function warmupDb(): Promise<boolean> {
55
+ try {
56
+ const database = getDb()
57
+ await database.execute(sql`SELECT 1`)
58
+ setDbState(true)
59
+ console.log('[db] Connection verified')
60
+ return true
61
+ } catch (error) {
62
+ console.error('[db] Warmup failed:', error)
63
+ setDbState(false, error instanceof Error ? error.message : 'Connection failed')
64
+ return false
65
+ }
66
+ }
67
+
68
+ export async function closeDb(): Promise<void> {
69
+ if (queryClient) {
70
+ await queryClient.end()
71
+ queryClient = null
72
+ db = null
73
+ }
74
+ }
75
+
76
+ export { schema }
@@ -0,0 +1,41 @@
1
+ import { migrate } from 'drizzle-orm/postgres-js/migrator'
2
+ import { sql } from 'drizzle-orm'
3
+ import { getDb } from './index'
4
+
5
+ const MIGRATION_LOCK_ID = 728492 // Arbitrary but consistent ID
6
+
7
+ export async function runMigrations(): Promise<boolean> {
8
+ const db = getDb()
9
+
10
+ console.log('[db] Attempting to acquire migration lock...')
11
+
12
+ try {
13
+ // Try to acquire advisory lock (non-blocking)
14
+ const lockResult = await db.execute<{ pg_try_advisory_lock: boolean }>(
15
+ sql`SELECT pg_try_advisory_lock(${MIGRATION_LOCK_ID})`
16
+ )
17
+
18
+ const acquired = lockResult[0]?.pg_try_advisory_lock === true
19
+
20
+ if (!acquired) {
21
+ console.log('[db] Migration lock held by another instance, skipping')
22
+ return false
23
+ }
24
+
25
+ console.log('[db] Lock acquired, running migrations...')
26
+ await migrate(db, { migrationsFolder: './server/drizzle/migrations' })
27
+ console.log('[db] Migrations complete')
28
+
29
+ return true
30
+ } catch (error) {
31
+ console.error('[db] Migration failed:', error)
32
+ throw error
33
+ } finally {
34
+ // Always release the lock
35
+ try {
36
+ await db.execute(sql`SELECT pg_advisory_unlock(${MIGRATION_LOCK_ID})`)
37
+ } catch {
38
+ // Ignore unlock errors
39
+ }
40
+ }
41
+ }