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,83 @@
1
+ import { desc, eq, sql } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+ import type { MemoryChunk, MemoryContextResponse } from '~~/shared/types'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const query = getQuery(event)
10
+ const db = getDb()
11
+
12
+ const projectPath = query.project as string | undefined
13
+ const limit = Math.min(parseInt(query.limit as string) || 5, 20)
14
+
15
+ // Get recent, high-relevance memories for this project
16
+ let dbQuery = db.select()
17
+ .from(schema.memoryChunks)
18
+
19
+ if (projectPath)
20
+ dbQuery = dbQuery.where(eq(schema.memoryChunks.projectPath, projectPath)) as typeof dbQuery
21
+
22
+ const memories = await dbQuery
23
+ .orderBy(
24
+ desc(schema.memoryChunks.relevanceScore),
25
+ desc(schema.memoryChunks.createdAt)
26
+ )
27
+ .limit(limit)
28
+
29
+ // Update access count for retrieved memories
30
+ if (memories.length > 0) {
31
+ const ids = memories.map(m => m.id)
32
+ await db.execute(sql`
33
+ UPDATE memory_chunks
34
+ SET access_count = access_count + 1,
35
+ last_accessed_at = NOW()
36
+ WHERE id = ANY(${ids})
37
+ `)
38
+ }
39
+
40
+ // Format for Claude context injection
41
+ const formatted = formatForContext(memories as MemoryChunk[])
42
+
43
+ const response: MemoryContextResponse = {
44
+ memories: memories as MemoryChunk[],
45
+ formatted
46
+ }
47
+
48
+ return { data: response }
49
+ })
50
+
51
+ function formatForContext(memories: MemoryChunk[]): string {
52
+ if (memories.length === 0)
53
+ return ''
54
+
55
+ const lines = ['## Previous Context\n']
56
+
57
+ // Group by type
58
+ const byType = memories.reduce((acc, m) => {
59
+ if (!acc[m.chunkType])
60
+ acc[m.chunkType] = []
61
+ acc[m.chunkType]!.push(m)
62
+ return acc
63
+ }, {} as Record<string, MemoryChunk[]>)
64
+
65
+ const typeLabels: Record<string, string> = {
66
+ decision: 'Decisions',
67
+ fact: 'Key Facts',
68
+ solution: 'Solutions',
69
+ pattern: 'Patterns',
70
+ preference: 'Preferences',
71
+ summary: 'Summaries'
72
+ }
73
+
74
+ for (const [type, items] of Object.entries(byType)) {
75
+ const label = typeLabels[type] || type
76
+ lines.push(`### ${label}`)
77
+ for (const item of items)
78
+ lines.push(`- ${item.content}`)
79
+ lines.push('')
80
+ }
81
+
82
+ return lines.join('\n')
83
+ }
@@ -0,0 +1,42 @@
1
+ import { getDb, schema } from '~~/server/db'
2
+ import { requireDb } from '~~/server/utils/db-guard'
3
+ import { extractMemories, extractMemoriesFromTranscriptFile } from '~~/server/services/memory-extractor'
4
+ import type { ExtractMemoryInput, MemoryChunk } from '~~/shared/types'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const body = await readBody<ExtractMemoryInput & { transcriptPath?: string }>(event)
10
+ const db = getDb()
11
+
12
+ let memories: Awaited<ReturnType<typeof extractMemories>> = []
13
+
14
+ // Extract from transcript file path or raw transcript
15
+ if (body.transcriptPath)
16
+ memories = await extractMemoriesFromTranscriptFile(body.transcriptPath)
17
+ else if (body.transcript)
18
+ memories = await extractMemories(body.transcript)
19
+ else
20
+ throw createError({ statusCode: 400, message: 'Either transcript or transcriptPath is required' })
21
+
22
+ if (memories.length === 0)
23
+ return { data: [], message: 'No memories worth extracting' }
24
+
25
+ // Store extracted memories
26
+ const inserted = await db.insert(schema.memoryChunks)
27
+ .values(memories.map(m => ({
28
+ sessionId: body.sessionId,
29
+ projectPath: body.projectPath,
30
+ chunkType: m.type,
31
+ content: m.content,
32
+ relevanceScore: m.relevance
33
+ })))
34
+ .returning()
35
+
36
+ console.log(`[memory] Extracted and stored ${inserted.length} memories`)
37
+
38
+ return {
39
+ data: inserted as MemoryChunk[],
40
+ message: `Extracted ${inserted.length} memories`
41
+ }
42
+ })
@@ -0,0 +1,70 @@
1
+ import { sql, desc, eq, and, gte, ilike, or } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+ import type { MemoryChunk, MemoryChunkType } from '~~/shared/types'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const query = getQuery(event)
10
+ const db = getDb()
11
+
12
+ const searchQuery = query.query as string | undefined
13
+ const projectPath = query.projectPath as string | undefined
14
+ const chunkType = query.chunkType as MemoryChunkType | undefined
15
+ const minRelevance = query.minRelevance ? parseFloat(query.minRelevance as string) : undefined
16
+ const limit = Math.min(parseInt(query.limit as string) || 20, 100)
17
+
18
+ const conditions = []
19
+
20
+ // Project filter
21
+ if (projectPath)
22
+ conditions.push(eq(schema.memoryChunks.projectPath, projectPath))
23
+
24
+ // Type filter
25
+ if (chunkType)
26
+ conditions.push(eq(schema.memoryChunks.chunkType, chunkType))
27
+
28
+ // Relevance filter
29
+ if (minRelevance !== undefined)
30
+ conditions.push(gte(schema.memoryChunks.relevanceScore, minRelevance))
31
+
32
+ // Text search (simple ILIKE for now, will upgrade to tsvector)
33
+ if (searchQuery) {
34
+ conditions.push(or(
35
+ ilike(schema.memoryChunks.content, `%${searchQuery}%`),
36
+ ilike(schema.memoryChunks.sourceExcerpt, `%${searchQuery}%`)
37
+ ))
38
+ }
39
+
40
+ let dbQuery = db.select()
41
+ .from(schema.memoryChunks)
42
+
43
+ if (conditions.length > 0)
44
+ dbQuery = dbQuery.where(and(...conditions)) as typeof dbQuery
45
+
46
+ const memories = await dbQuery
47
+ .orderBy(
48
+ desc(schema.memoryChunks.relevanceScore),
49
+ desc(schema.memoryChunks.createdAt)
50
+ )
51
+ .limit(limit)
52
+
53
+ // Update access count and timestamp for retrieved memories
54
+ if (memories.length > 0) {
55
+ try {
56
+ const ids = memories.map(m => m.id)
57
+ await db.execute(sql`
58
+ UPDATE memory_chunks
59
+ SET access_count = access_count + 1,
60
+ last_accessed_at = NOW()
61
+ WHERE id = ANY(ARRAY[${sql.join(ids.map(id => sql`${id}`), sql`, `)}]::uuid[])
62
+ `)
63
+ } catch (e) {
64
+ // Don't fail the request if access count update fails
65
+ console.error('[memory/search] Failed to update access count:', e)
66
+ }
67
+ }
68
+
69
+ return { data: memories as MemoryChunk[] }
70
+ })
@@ -0,0 +1,31 @@
1
+ import { getDb, schema } from '~~/server/db'
2
+ import { requireDb } from '~~/server/utils/db-guard'
3
+ import type { CreateMemoryInput, MemoryChunk } from '~~/shared/types'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ requireDb(event)
7
+
8
+ const body = await readBody<CreateMemoryInput>(event)
9
+ const db = getDb()
10
+
11
+ if (!body.content)
12
+ throw createError({ statusCode: 400, message: 'content is required' })
13
+
14
+ if (!body.chunkType)
15
+ throw createError({ statusCode: 400, message: 'chunkType is required' })
16
+
17
+ const [inserted] = await db.insert(schema.memoryChunks)
18
+ .values({
19
+ sessionId: body.sessionId,
20
+ projectPath: body.projectPath,
21
+ chunkType: body.chunkType,
22
+ content: body.content,
23
+ sourceExcerpt: body.sourceExcerpt,
24
+ relevanceScore: body.relevanceScore ?? 1.0
25
+ })
26
+ .returning()
27
+
28
+ console.log(`[memory] Stored memory: ${body.chunkType} - ${body.content.slice(0, 50)}...`)
29
+
30
+ return { data: inserted as MemoryChunk }
31
+ })
@@ -0,0 +1,40 @@
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: 'Project ID is required' })
12
+
13
+ const db = getDb()
14
+
15
+ // Check project exists
16
+ const [existing] = await db
17
+ .select({ id: schema.projects.id })
18
+ .from(schema.projects)
19
+ .where(eq(schema.projects.id, id))
20
+ .limit(1)
21
+
22
+ if (!existing)
23
+ throw createError({ statusCode: 404, message: 'Project not found' })
24
+
25
+ const userId = event.context.user?.id
26
+
27
+ // Soft delete
28
+ const [project] = await db
29
+ .update(schema.projects)
30
+ .set({
31
+ deletedAt: new Date(),
32
+ deletedBy: userId,
33
+ modifiedAt: new Date(),
34
+ modifiedBy: userId
35
+ })
36
+ .where(eq(schema.projects.id, id))
37
+ .returning()
38
+
39
+ return { data: project }
40
+ })
@@ -0,0 +1,25 @@
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: 'Project ID is required' })
12
+
13
+ const db = getDb()
14
+
15
+ const [project] = await db
16
+ .select()
17
+ .from(schema.projects)
18
+ .where(eq(schema.projects.id, id))
19
+ .limit(1)
20
+
21
+ if (!project)
22
+ throw createError({ statusCode: 404, message: 'Project not found' })
23
+
24
+ return { data: project }
25
+ })
@@ -0,0 +1,50 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+ import type { UpdateProjectInput } 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: 'Project ID is required' })
13
+
14
+ const body = await readBody<UpdateProjectInput>(event)
15
+
16
+ // Validate color if provided
17
+ if (body.color && !/^#[0-9A-Fa-f]{6}$/.test(body.color))
18
+ throw createError({ statusCode: 400, message: 'Color must be a valid hex color (e.g., #3b82f6)' })
19
+
20
+ const db = getDb()
21
+
22
+ // Check project exists
23
+ const [existing] = await db
24
+ .select({ id: schema.projects.id })
25
+ .from(schema.projects)
26
+ .where(eq(schema.projects.id, id))
27
+ .limit(1)
28
+
29
+ if (!existing)
30
+ throw createError({ statusCode: 404, message: 'Project 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.name !== undefined) updates.name = body.name.trim()
40
+ if (body.color !== undefined) updates.color = body.color
41
+ if (body.description !== undefined) updates.description = body.description?.trim() || null
42
+
43
+ const [project] = await db
44
+ .update(schema.projects)
45
+ .set(updates)
46
+ .where(eq(schema.projects.id, id))
47
+ .returning()
48
+
49
+ return { data: project }
50
+ })
@@ -0,0 +1,20 @@
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 query = getQuery(event)
9
+ const includeDeleted = query.includeDeleted === 'true'
10
+
11
+ const db = getDb()
12
+
13
+ const projects = await db
14
+ .select()
15
+ .from(schema.projects)
16
+ .where(includeDeleted ? undefined : isNull(schema.projects.deletedAt))
17
+ .orderBy(schema.projects.name)
18
+
19
+ return { data: projects }
20
+ })
@@ -0,0 +1,34 @@
1
+ import { getDb, schema } from '~~/server/db'
2
+ import { requireDb } from '~~/server/utils/db-guard'
3
+ import type { CreateProjectInput } from '~~/shared/types'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ requireDb(event)
7
+
8
+ const body = await readBody<CreateProjectInput>(event)
9
+
10
+ if (!body.name?.trim())
11
+ throw createError({ statusCode: 400, message: 'Project name is required' })
12
+
13
+ if (!body.color?.trim())
14
+ throw createError({ statusCode: 400, message: 'Project color is required' })
15
+
16
+ // Validate hex color format
17
+ if (!/^#[0-9A-Fa-f]{6}$/.test(body.color))
18
+ throw createError({ statusCode: 400, message: 'Color must be a valid hex color (e.g., #3b82f6)' })
19
+
20
+ const db = getDb()
21
+ const userId = event.context.user?.id
22
+
23
+ const [project] = await db
24
+ .insert(schema.projects)
25
+ .values({
26
+ name: body.name.trim(),
27
+ color: body.color,
28
+ description: body.description?.trim() || null,
29
+ createdBy: userId
30
+ })
31
+ .returning()
32
+
33
+ return { data: project }
34
+ })
@@ -0,0 +1,31 @@
1
+ import { getDb } from '~~/server/db'
2
+ import { secrets } from '~~/server/db/schema'
3
+ import { eq } from 'drizzle-orm'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const key = getRouterParam(event, 'key')
7
+
8
+ if (!key) {
9
+ throw createError({
10
+ statusCode: 400,
11
+ message: 'Key is required'
12
+ })
13
+ }
14
+
15
+ const db = getDb()
16
+
17
+ const existing = await db.query.secrets.findFirst({
18
+ where: (s, { eq }) => eq(s.key, key)
19
+ })
20
+
21
+ if (!existing) {
22
+ throw createError({
23
+ statusCode: 404,
24
+ message: `Secret with key "${key}" not found`
25
+ })
26
+ }
27
+
28
+ await db.delete(secrets).where(eq(secrets.key, key))
29
+
30
+ return { data: { deleted: true } }
31
+ })
@@ -0,0 +1,30 @@
1
+ import { getDb } from '~~/server/db'
2
+ import { decryptSecret } from '~~/server/utils/crypto'
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const key = getRouterParam(event, 'key')
6
+
7
+ if (!key) {
8
+ throw createError({
9
+ statusCode: 400,
10
+ message: 'Key is required'
11
+ })
12
+ }
13
+
14
+ const db = getDb()
15
+
16
+ const secret = await db.query.secrets.findFirst({
17
+ where: (s, { eq }) => eq(s.key, key)
18
+ })
19
+
20
+ if (!secret) {
21
+ throw createError({
22
+ statusCode: 404,
23
+ message: `Secret with key "${key}" not found`
24
+ })
25
+ }
26
+
27
+ const value = decryptSecret(secret.encryptedValue, secret.iv)
28
+
29
+ return { data: { key: secret.key, value } }
30
+ })
@@ -0,0 +1,52 @@
1
+ import { getDb } from '~~/server/db'
2
+ import { secrets } from '~~/server/db/schema'
3
+ import { encryptSecret } from '~~/server/utils/crypto'
4
+ import { eq } from 'drizzle-orm'
5
+
6
+ interface UpdateSecretInput {
7
+ value?: string
8
+ description?: string
9
+ }
10
+
11
+ export default defineEventHandler(async (event) => {
12
+ const key = getRouterParam(event, 'key')
13
+
14
+ if (!key)
15
+ throw createError({ statusCode: 400, message: 'Key is required' })
16
+
17
+ const body = await readBody<UpdateSecretInput>(event)
18
+ const db = getDb()
19
+
20
+ const existing = await db.query.secrets.findFirst({
21
+ where: (s, { eq }) => eq(s.key, key)
22
+ })
23
+
24
+ if (!existing)
25
+ throw createError({ statusCode: 404, message: `Secret with key "${key}" not found` })
26
+
27
+ const updateData: Record<string, unknown> = {
28
+ updatedAt: new Date()
29
+ }
30
+
31
+ if (body.value) {
32
+ const { encrypted, iv } = encryptSecret(body.value)
33
+ updateData.encryptedValue = encrypted
34
+ updateData.iv = iv
35
+ }
36
+
37
+ if (body.description !== undefined) {
38
+ updateData.description = body.description?.trim() || null
39
+ }
40
+
41
+ const [result] = await db.update(secrets)
42
+ .set(updateData)
43
+ .where(eq(secrets.key, key))
44
+ .returning({
45
+ id: secrets.id,
46
+ key: secrets.key,
47
+ description: secrets.description,
48
+ updatedAt: secrets.updatedAt
49
+ })
50
+
51
+ return { data: result }
52
+ })
@@ -0,0 +1,20 @@
1
+ import { getDb } from '~~/server/db'
2
+ import { secrets } from '~~/server/db/schema'
3
+ import { desc } from 'drizzle-orm'
4
+
5
+ export default defineEventHandler(async () => {
6
+ const db = getDb()
7
+
8
+ const results = await db.query.secrets.findMany({
9
+ columns: {
10
+ id: true,
11
+ key: true,
12
+ description: true,
13
+ createdAt: true,
14
+ updatedAt: true
15
+ },
16
+ orderBy: [desc(secrets.createdAt)]
17
+ })
18
+
19
+ return { data: results }
20
+ })
@@ -0,0 +1,58 @@
1
+ import { getDb } from '~~/server/db'
2
+ import { secrets } from '~~/server/db/schema'
3
+ import { encryptSecret } from '~~/server/utils/crypto'
4
+
5
+ interface CreateSecretInput {
6
+ key: string
7
+ value: string
8
+ description?: string
9
+ }
10
+
11
+ export default defineEventHandler(async (event) => {
12
+ const body = await readBody<CreateSecretInput>(event)
13
+
14
+ if (!body.key?.trim())
15
+ throw createError({ statusCode: 400, message: 'Key is required' })
16
+
17
+ if (!body.value)
18
+ throw createError({ statusCode: 400, message: 'Value is required' })
19
+
20
+ // Validate key format: SCREAMING_SNAKE_CASE
21
+ if (!/^[A-Z][A-Z0-9_]*$/.test(body.key))
22
+ throw createError({ statusCode: 400, message: 'Key must be SCREAMING_SNAKE_CASE (e.g., MY_API_KEY)' })
23
+
24
+ if (body.key.length > 255)
25
+ throw createError({ statusCode: 400, message: 'Key must be 255 characters or less' })
26
+
27
+ const db = getDb()
28
+ const userId = event.context.user?.id
29
+
30
+ // Check for duplicate key
31
+ const existing = await db.query.secrets.findFirst({
32
+ where: (s, { eq }) => eq(s.key, body.key)
33
+ })
34
+
35
+ if (existing) {
36
+ throw createError({
37
+ statusCode: 409,
38
+ message: `Secret with key "${body.key}" already exists`
39
+ })
40
+ }
41
+
42
+ const { encrypted, iv } = encryptSecret(body.value)
43
+
44
+ const [result] = await db.insert(secrets).values({
45
+ key: body.key,
46
+ encryptedValue: encrypted,
47
+ iv,
48
+ description: body.description?.trim() || null,
49
+ createdBy: userId
50
+ }).returning({
51
+ id: secrets.id,
52
+ key: secrets.key,
53
+ description: secrets.description,
54
+ createdAt: secrets.createdAt
55
+ })
56
+
57
+ return { data: result }
58
+ })
@@ -0,0 +1,46 @@
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 })
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
+ const userId = event.context.user?.id
26
+
27
+ // Soft delete
28
+ await db
29
+ .update(schema.tasks)
30
+ .set({
31
+ deletedAt: new Date(),
32
+ deletedBy: userId,
33
+ modifiedAt: new Date(),
34
+ modifiedBy: userId
35
+ })
36
+ .where(eq(schema.tasks.id, id))
37
+
38
+ // Fetch updated task with project relation
39
+ const [task] = await db.query.tasks.findMany({
40
+ where: (tasks, { eq }) => eq(tasks.id, id),
41
+ with: { project: true },
42
+ limit: 1
43
+ })
44
+
45
+ return { data: task }
46
+ })
@@ -0,0 +1,24 @@
1
+ import { getDb } from '~~/server/db'
2
+ import { requireDb } from '~~/server/utils/db-guard'
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ requireDb(event)
6
+
7
+ const id = getRouterParam(event, 'id')
8
+
9
+ if (!id)
10
+ throw createError({ statusCode: 400, message: 'Task ID is required' })
11
+
12
+ const db = getDb()
13
+
14
+ const [task] = await db.query.tasks.findMany({
15
+ where: (tasks, { eq }) => eq(tasks.id, id),
16
+ with: { project: true, creator: true },
17
+ limit: 1
18
+ })
19
+
20
+ if (!task)
21
+ throw createError({ statusCode: 404, message: 'Task not found' })
22
+
23
+ return { data: task }
24
+ })