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,16 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb } from '~~/server/db'
3
+ import * as schema from '~~/server/db/schema'
4
+ import { requireDb } from '~~/server/utils/db-guard'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const id = getRouterParam(event, 'id')!
10
+ const db = getDb()
11
+
12
+ await db.delete(schema.conversations)
13
+ .where(eq(schema.conversations.id, id))
14
+
15
+ return { data: { success: true } }
16
+ })
@@ -0,0 +1,34 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb } from '~~/server/db'
3
+ import * as schema from '~~/server/db/schema'
4
+ import { requireDb } from '~~/server/utils/db-guard'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const id = getRouterParam(event, 'id')!
10
+ const db = getDb()
11
+
12
+ const [conversation] = await db.select()
13
+ .from(schema.conversations)
14
+ .where(eq(schema.conversations.id, id))
15
+ .limit(1)
16
+
17
+ if (!conversation)
18
+ throw createError({ statusCode: 404, message: 'Conversation not found' })
19
+
20
+ const messages = await db.select()
21
+ .from(schema.conversationMessages)
22
+ .where(eq(schema.conversationMessages.conversationId, id))
23
+ .orderBy(schema.conversationMessages.createdAt)
24
+
25
+ return {
26
+ data: {
27
+ ...conversation,
28
+ messages: messages.map(m => ({
29
+ ...m,
30
+ content: typeof m.content === 'string' ? JSON.parse(m.content) : m.content
31
+ }))
32
+ }
33
+ }
34
+ })
@@ -0,0 +1,17 @@
1
+ import { desc } from 'drizzle-orm'
2
+ import { getDb } from '~~/server/db'
3
+ import * as schema from '~~/server/db/schema'
4
+ import { requireDb } from '~~/server/utils/db-guard'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const db = getDb()
10
+
11
+ const conversations = await db.select()
12
+ .from(schema.conversations)
13
+ .orderBy(desc(schema.conversations.startedAt))
14
+ .limit(50)
15
+
16
+ return { data: conversations }
17
+ })
@@ -0,0 +1,47 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { rm, stat } from 'fs/promises'
3
+ import { getDb } from '~~/server/db'
4
+ import * as schema from '~~/server/db/schema'
5
+ import { requireDb } from '~~/server/utils/db-guard'
6
+ import { validatePath } from '~~/server/utils/path-validator'
7
+
8
+ export default defineEventHandler(async (event) => {
9
+ requireDb(event)
10
+
11
+ const id = getRouterParam(event, 'id')
12
+ if (!id)
13
+ throw createError({ statusCode: 400, message: 'Document ID is required' })
14
+
15
+ const db = getDb()
16
+ const userId = event.context.user?.id
17
+
18
+ const document = await db.query.documents.findFirst({
19
+ where: eq(schema.documents.id, id)
20
+ })
21
+
22
+ if (!document)
23
+ throw createError({ statusCode: 404, message: 'Document not found' })
24
+
25
+ const absolutePath = validatePath(document.path)
26
+
27
+ // Delete file from disk
28
+ try {
29
+ await stat(absolutePath)
30
+ await rm(absolutePath)
31
+ } catch (error: unknown) {
32
+ const err = error as NodeJS.ErrnoException
33
+ if (err.code !== 'ENOENT') {
34
+ console.error('Failed to delete file:', err)
35
+ }
36
+ }
37
+
38
+ // Soft delete in database
39
+ await db.update(schema.documents).set({
40
+ deletedAt: new Date(),
41
+ deletedBy: userId,
42
+ modifiedAt: new Date(),
43
+ modifiedBy: userId
44
+ }).where(eq(schema.documents.id, id))
45
+
46
+ return { data: { id, deleted: true } }
47
+ })
@@ -0,0 +1,102 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { readFile, writeFile } from 'fs/promises'
3
+ import { basename } from 'path'
4
+ import { getDb } from '~~/server/db'
5
+ import * as schema from '~~/server/db/schema'
6
+ import { requireDb } from '~~/server/utils/db-guard'
7
+ import { validatePath } from '~~/server/utils/path-validator'
8
+ import { parseFrontmatter, stringifyFrontmatter, computeContentHash, extractTitle } from '~~/server/utils/frontmatter'
9
+
10
+ export default defineEventHandler(async (event) => {
11
+ requireDb(event)
12
+
13
+ const id = getRouterParam(event, 'id')
14
+ if (!id)
15
+ throw createError({ statusCode: 400, message: 'Document ID is required' })
16
+
17
+ const body = await readBody(event)
18
+ const db = getDb()
19
+ const userId = event.context.user?.id
20
+
21
+ const document = await db.query.documents.findFirst({
22
+ where: eq(schema.documents.id, id)
23
+ })
24
+
25
+ if (!document)
26
+ throw createError({ statusCode: 404, message: 'Document not found' })
27
+
28
+ if (document.fileType === 'binary')
29
+ throw createError({ statusCode: 400, message: 'Cannot edit binary file metadata' })
30
+
31
+ const absolutePath = validatePath(document.path)
32
+
33
+ // Read current file
34
+ let fileContent: string
35
+ try {
36
+ fileContent = await readFile(absolutePath, 'utf-8')
37
+ } catch {
38
+ throw createError({ statusCode: 500, message: 'Failed to read file' })
39
+ }
40
+
41
+ const { metadata: currentMetadata, body: currentBody } = parseFrontmatter(fileContent)
42
+
43
+ // Build new metadata
44
+ const newMetadata: Record<string, unknown> = { ...currentMetadata }
45
+
46
+ if (body.title !== undefined) newMetadata.title = body.title
47
+ if (body.tags !== undefined) newMetadata.tags = body.tags
48
+ if (body.projectId !== undefined) newMetadata.project = body.projectId
49
+ if (body.shared !== undefined) newMetadata.shared = body.shared
50
+ if (body.shareType !== undefined) newMetadata.shareType = body.shareType
51
+
52
+ // Use new body if provided, otherwise keep current
53
+ const newBody = body.body !== undefined ? body.body : currentBody
54
+
55
+ // Reconstruct file content
56
+ const newFileContent = stringifyFrontmatter(newMetadata, newBody)
57
+ const newHash = computeContentHash(newFileContent)
58
+ const filename = basename(document.path)
59
+ const newTitle = extractTitle(newMetadata, newBody, filename)
60
+
61
+ // Write file
62
+ try {
63
+ await writeFile(absolutePath, newFileContent, 'utf-8')
64
+ } catch {
65
+ throw createError({ statusCode: 500, message: 'Failed to write file' })
66
+ }
67
+
68
+ // Update database
69
+ await db.update(schema.documents).set({
70
+ title: newTitle,
71
+ content: newBody,
72
+ contentHash: newHash,
73
+ tags: Array.isArray(newMetadata.tags) ? newMetadata.tags as string[] : document.tags,
74
+ projectId: body.projectId !== undefined ? body.projectId : document.projectId,
75
+ shared: body.shared !== undefined ? body.shared : document.shared,
76
+ shareType: body.shareType !== undefined ? body.shareType : document.shareType,
77
+ syncedAt: new Date(),
78
+ modifiedAt: new Date(),
79
+ modifiedBy: userId
80
+ }).where(eq(schema.documents.id, id))
81
+
82
+ // Return updated document
83
+ const updated = await db.query.documents.findFirst({
84
+ where: eq(schema.documents.id, id),
85
+ with: { project: true }
86
+ })
87
+
88
+ return {
89
+ data: {
90
+ document: updated,
91
+ metadata: {
92
+ title: updated!.title,
93
+ tags: updated!.tags || [],
94
+ projectId: updated!.projectId,
95
+ shared: updated!.shared,
96
+ shareType: updated!.shareType,
97
+ ...newMetadata
98
+ },
99
+ body: newBody
100
+ }
101
+ }
102
+ })
@@ -0,0 +1,60 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb } from '~~/server/db'
3
+ import * as schema from '~~/server/db/schema'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const id = getRouterParam(event, 'id')
7
+ if (!id)
8
+ throw createError({ statusCode: 400, message: 'Document ID is required' })
9
+
10
+ // Validate UUID format
11
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
12
+ if (!uuidRegex.test(id))
13
+ throw createError({ statusCode: 400, message: 'Invalid document ID format' })
14
+
15
+ const db = getDb()
16
+ const user = event.context.user // May be null for unauthenticated requests
17
+
18
+ const document = await db.query.documents.findFirst({
19
+ where: eq(schema.documents.id, id),
20
+ with: {
21
+ creator: {
22
+ columns: { id: true, name: true }
23
+ }
24
+ }
25
+ })
26
+
27
+ // 404 if not found or deleted
28
+ if (!document || document.deletedAt)
29
+ throw createError({ statusCode: 404, message: 'Document not found' })
30
+
31
+ const isOwner = user?.id === document.createdBy
32
+
33
+ // Access control: owner always has access, otherwise check sharing settings
34
+ if (!isOwner) {
35
+ if (!document.shared)
36
+ throw createError({ statusCode: 403, message: 'This document is not shared' })
37
+
38
+ // shared=true with any shareType (public or private) allows access
39
+ // Having the UUID = having the link for private documents
40
+ }
41
+
42
+ return {
43
+ data: {
44
+ document: {
45
+ id: document.id,
46
+ title: document.title,
47
+ path: document.path,
48
+ fileType: document.fileType,
49
+ shared: document.shared,
50
+ shareType: document.shareType,
51
+ tags: document.tags || [],
52
+ createdAt: document.createdAt,
53
+ modifiedAt: document.modifiedAt,
54
+ creatorName: document.creator?.name || null
55
+ },
56
+ content: document.fileType !== 'binary' ? document.content : null,
57
+ isOwner
58
+ }
59
+ }
60
+ })
@@ -0,0 +1,65 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { writeFile, mkdir } from 'fs/promises'
3
+ import { dirname } from 'path'
4
+ import { getDb } from '~~/server/db'
5
+ import * as schema from '~~/server/db/schema'
6
+ import { requireDb } from '~~/server/utils/db-guard'
7
+ import { validatePath } from '~~/server/utils/path-validator'
8
+ import { stringifyFrontmatter } from '~~/server/utils/frontmatter'
9
+
10
+ export default defineEventHandler(async (event) => {
11
+ requireDb(event)
12
+
13
+ const id = getRouterParam(event, 'id')
14
+ if (!id)
15
+ throw createError({ statusCode: 400, message: 'Document ID is required' })
16
+
17
+ const db = getDb()
18
+ const userId = event.context.user?.id
19
+
20
+ const document = await db.query.documents.findFirst({
21
+ where: eq(schema.documents.id, id)
22
+ })
23
+
24
+ if (!document)
25
+ throw createError({ statusCode: 404, message: 'Document not found' })
26
+
27
+ if (!document.deletedAt)
28
+ throw createError({ statusCode: 400, message: 'Document is not deleted' })
29
+
30
+ const absolutePath = validatePath(document.path)
31
+
32
+ // Recreate file if we have content
33
+ if (document.fileType === 'markdown' && document.content) {
34
+ const metadata: Record<string, unknown> = {}
35
+ if (document.title) metadata.title = document.title
36
+ if (document.tags?.length) metadata.tags = document.tags
37
+ if (document.projectId) metadata.project = document.projectId
38
+ if (document.shared) metadata.shared = document.shared
39
+ if (document.shareType) metadata.shareType = document.shareType
40
+
41
+ const fileContent = stringifyFrontmatter(metadata, document.content)
42
+
43
+ try {
44
+ await mkdir(dirname(absolutePath), { recursive: true })
45
+ await writeFile(absolutePath, fileContent, 'utf-8')
46
+ } catch {
47
+ throw createError({ statusCode: 500, message: 'Failed to restore file' })
48
+ }
49
+ }
50
+
51
+ // Clear soft delete
52
+ await db.update(schema.documents).set({
53
+ deletedAt: null,
54
+ deletedBy: null,
55
+ modifiedAt: new Date(),
56
+ modifiedBy: userId
57
+ }).where(eq(schema.documents.id, id))
58
+
59
+ const restored = await db.query.documents.findFirst({
60
+ where: eq(schema.documents.id, id),
61
+ with: { project: true }
62
+ })
63
+
64
+ return { data: restored }
65
+ })
@@ -0,0 +1,168 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { readFile } from 'fs/promises'
3
+ import { basename } from 'path'
4
+ import { getDb } from '~~/server/db'
5
+ import * as schema from '~~/server/db/schema'
6
+ import { requireDb } from '~~/server/utils/db-guard'
7
+ import { validatePath } from '~~/server/utils/path-validator'
8
+ import { parseFrontmatter, extractTitle, computeContentHash, getFileType, getMimeType } from '~~/server/utils/frontmatter'
9
+
10
+ export default defineEventHandler(async (event) => {
11
+ requireDb(event)
12
+
13
+ const body = await readBody(event)
14
+ const requestedPath = body.path
15
+
16
+ if (!requestedPath)
17
+ throw createError({ statusCode: 400, message: 'Path is required' })
18
+
19
+ const absolutePath = validatePath(requestedPath)
20
+ const db = getDb()
21
+ const filename = basename(requestedPath)
22
+
23
+ // Check if document exists in DB
24
+ let document = await db.query.documents.findFirst({
25
+ where: eq(schema.documents.path, requestedPath),
26
+ with: { project: true }
27
+ })
28
+
29
+ // Read file from disk
30
+ let fileContent: string
31
+ try {
32
+ fileContent = await readFile(absolutePath, 'utf-8')
33
+ } catch (error: unknown) {
34
+ const err = error as NodeJS.ErrnoException
35
+ if (err.code === 'ENOENT')
36
+ throw createError({ statusCode: 404, message: 'File not found' })
37
+ throw createError({ statusCode: 500, message: 'Failed to read file' })
38
+ }
39
+
40
+ const fileType = getFileType(filename)
41
+
42
+ // Handle binary files
43
+ if (fileType === 'binary') {
44
+ if (!document) {
45
+ const [newDoc] = await db.insert(schema.documents).values({
46
+ path: requestedPath,
47
+ title: filename,
48
+ fileType: 'binary',
49
+ mimeType: getMimeType(filename),
50
+ syncedAt: new Date(),
51
+ createdBy: event.context.user?.id
52
+ }).returning()
53
+
54
+ document = await db.query.documents.findFirst({
55
+ where: eq(schema.documents.id, newDoc!.id),
56
+ with: { project: true }
57
+ })
58
+ }
59
+
60
+ return {
61
+ data: {
62
+ document,
63
+ metadata: { title: document!.title, tags: [], shared: false },
64
+ body: ''
65
+ }
66
+ }
67
+ }
68
+
69
+ // Handle text files (non-markdown) - no frontmatter parsing
70
+ if (fileType === 'text') {
71
+ const contentHash = computeContentHash(fileContent)
72
+
73
+ if (!document) {
74
+ const [newDoc] = await db.insert(schema.documents).values({
75
+ path: requestedPath,
76
+ title: filename,
77
+ content: fileContent,
78
+ contentHash,
79
+ fileType: 'text',
80
+ syncedAt: new Date(),
81
+ createdBy: event.context.user?.id
82
+ }).returning()
83
+
84
+ document = await db.query.documents.findFirst({
85
+ where: eq(schema.documents.id, newDoc!.id),
86
+ with: { project: true }
87
+ })
88
+ } else if (document.contentHash !== contentHash) {
89
+ await db.update(schema.documents).set({
90
+ content: fileContent,
91
+ contentHash,
92
+ syncedAt: new Date(),
93
+ modifiedAt: new Date(),
94
+ modifiedBy: event.context.user?.id
95
+ }).where(eq(schema.documents.id, document.id))
96
+
97
+ document = await db.query.documents.findFirst({
98
+ where: eq(schema.documents.id, document.id),
99
+ with: { project: true }
100
+ })
101
+ }
102
+
103
+ return {
104
+ data: {
105
+ document,
106
+ metadata: { title: document!.title, tags: document!.tags || [], shared: document!.shared },
107
+ body: fileContent
108
+ }
109
+ }
110
+ }
111
+
112
+ // Handle markdown files - parse frontmatter
113
+ const { metadata, body: markdownBody } = parseFrontmatter(fileContent)
114
+ const contentHash = computeContentHash(fileContent)
115
+ const title = extractTitle(metadata, markdownBody, filename)
116
+
117
+ if (!document) {
118
+ const [newDoc] = await db.insert(schema.documents).values({
119
+ path: requestedPath,
120
+ title,
121
+ content: markdownBody,
122
+ contentHash,
123
+ tags: Array.isArray(metadata.tags) ? metadata.tags as string[] : [],
124
+ projectId: typeof metadata.project === 'string' ? metadata.project : null,
125
+ shared: !!metadata.shared,
126
+ shareType: metadata.shareType as 'public' | 'private' || null,
127
+ fileType: 'markdown',
128
+ syncedAt: new Date(),
129
+ createdBy: event.context.user?.id
130
+ }).returning()
131
+
132
+ document = await db.query.documents.findFirst({
133
+ where: eq(schema.documents.id, newDoc!.id),
134
+ with: { project: true }
135
+ })
136
+ } else if (document.contentHash !== contentHash) {
137
+ // File changed, update DB
138
+ await db.update(schema.documents).set({
139
+ title,
140
+ content: markdownBody,
141
+ contentHash,
142
+ tags: Array.isArray(metadata.tags) ? metadata.tags as string[] : document.tags,
143
+ syncedAt: new Date(),
144
+ modifiedAt: new Date(),
145
+ modifiedBy: event.context.user?.id
146
+ }).where(eq(schema.documents.id, document.id))
147
+
148
+ document = await db.query.documents.findFirst({
149
+ where: eq(schema.documents.id, document.id),
150
+ with: { project: true }
151
+ })
152
+ }
153
+
154
+ return {
155
+ data: {
156
+ document,
157
+ metadata: {
158
+ title: document!.title,
159
+ tags: document!.tags || [],
160
+ projectId: document!.projectId,
161
+ shared: document!.shared,
162
+ shareType: document!.shareType,
163
+ ...metadata
164
+ },
165
+ body: markdownBody
166
+ }
167
+ }
168
+ })
@@ -0,0 +1,48 @@
1
+ import { eq, isNull, ilike, and, or } from 'drizzle-orm'
2
+ import { getDb } from '~~/server/db'
3
+ import * as schema from '~~/server/db/schema'
4
+ import { requireDb } from '~~/server/utils/db-guard'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ requireDb(event)
8
+
9
+ const query = getQuery(event)
10
+ const includeDeleted = query.includeDeleted === 'true'
11
+ const projectId = query.projectId as string | undefined
12
+ const search = query.search as string | undefined
13
+ const fileType = query.fileType as string | undefined
14
+
15
+ const db = getDb()
16
+ const conditions = []
17
+
18
+ if (!includeDeleted)
19
+ conditions.push(isNull(schema.documents.deletedAt))
20
+
21
+ if (projectId)
22
+ conditions.push(eq(schema.documents.projectId, projectId))
23
+
24
+ if (fileType)
25
+ conditions.push(eq(schema.documents.fileType, fileType))
26
+
27
+ if (search) {
28
+ const pattern = `%${search}%`
29
+ conditions.push(
30
+ or(
31
+ ilike(schema.documents.title, pattern),
32
+ ilike(schema.documents.content, pattern),
33
+ ilike(schema.documents.path, pattern)
34
+ )
35
+ )
36
+ }
37
+
38
+ const documents = await db.query.documents.findMany({
39
+ where: conditions.length > 0 ? and(...conditions) : undefined,
40
+ with: { project: true },
41
+ orderBy: (docs, { desc }) => [desc(docs.modifiedAt)],
42
+ columns: {
43
+ content: false
44
+ }
45
+ })
46
+
47
+ return { data: documents }
48
+ })
@@ -0,0 +1,41 @@
1
+ import { rm, stat } from 'fs/promises'
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const body = await readBody(event)
5
+ const requestedPath = body.path
6
+
7
+ if (!requestedPath) {
8
+ throw createError({
9
+ statusCode: 400,
10
+ message: 'Path is required'
11
+ })
12
+ }
13
+
14
+ const absolutePath = validatePath(requestedPath)
15
+
16
+ // Check if path exists
17
+ try {
18
+ await stat(absolutePath)
19
+ } catch {
20
+ throw createError({
21
+ statusCode: 404,
22
+ message: 'Path not found'
23
+ })
24
+ }
25
+
26
+ try {
27
+ await rm(absolutePath, { recursive: true })
28
+
29
+ return {
30
+ data: {
31
+ path: requestedPath,
32
+ deleted: true
33
+ }
34
+ }
35
+ } catch {
36
+ throw createError({
37
+ statusCode: 500,
38
+ message: 'Failed to delete'
39
+ })
40
+ }
41
+ })