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.
- package/.env.example +58 -0
- package/Claude/CLAUDE.md +92 -0
- package/Claude/hooks/lib/__init__.py +1 -0
- package/Claude/hooks/lib/hook_client.py +207 -0
- package/Claude/hooks/log-event.py +97 -0
- package/Claude/hooks/pre-compact.py +46 -0
- package/Claude/hooks/session-end.py +26 -0
- package/Claude/hooks/session-start.py +35 -0
- package/Claude/hooks/stop-extract.py +40 -0
- package/Claude/rules/frontmatter.md +54 -0
- package/Claude/rules/markdown.md +43 -0
- package/Claude/rules/note-organization.md +33 -0
- package/Claude/settings.json +54 -0
- package/Claude/skills/README.md +136 -0
- package/Claude/skills/_lib/__init__.py +1 -0
- package/Claude/skills/_lib/api.py +164 -0
- package/Claude/skills/_lib/output.py +95 -0
- package/Claude/skills/environment/SKILL.md +73 -0
- package/Claude/skills/environment/environment.py +239 -0
- package/Claude/skills/memory/SKILL.md +153 -0
- package/Claude/skills/memory/memory.py +270 -0
- package/Claude/skills/project/SKILL.md +105 -0
- package/Claude/skills/project/project.py +203 -0
- package/Claude/skills/skill-creator/SKILL.md +261 -0
- package/Claude/skills/task/SKILL.md +135 -0
- package/Claude/skills/task/task.py +310 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +39 -0
- package/app/assets/css/main.css +10 -0
- package/app/components/AppLogo.vue +40 -0
- package/app/components/AssistantPanel.client.vue +518 -0
- package/app/components/ConfirmModal.vue +84 -0
- package/app/components/TemplateMenu.vue +49 -0
- package/app/components/agents/AgentActivityChart.client.vue +105 -0
- package/app/components/agents/AgentActivityChart.server.vue +25 -0
- package/app/components/agents/AgentForm.vue +304 -0
- package/app/components/agents/AgentRunModal.vue +154 -0
- package/app/components/agents/AgentStatsCards.vue +98 -0
- package/app/components/chat/ChatInput.vue +85 -0
- package/app/components/chat/ConversationList.vue +78 -0
- package/app/components/chat/MessageBubble.vue +81 -0
- package/app/components/chat/StreamingMessage.vue +36 -0
- package/app/components/chat/ToolCallBlock.vue +77 -0
- package/app/components/editor/CodeEditor.client.vue +212 -0
- package/app/components/editor/CodeEditorFallback.vue +12 -0
- package/app/components/editor/DocumentEditor.vue +326 -0
- package/app/components/editor/DocumentMetadata.vue +140 -0
- package/app/components/editor/MarkdownEditor.vue +146 -0
- package/app/components/files/FileTree.vue +436 -0
- package/app/components/hooks/HookActivityChart.client.vue +117 -0
- package/app/components/hooks/HookActivityChart.server.vue +25 -0
- package/app/components/hooks/HookStatsCards.vue +63 -0
- package/app/components/hooks/RecentEventsTable.vue +123 -0
- package/app/components/hooks/ToolBreakdownTable.vue +72 -0
- package/app/components/search/DashboardSearch.vue +122 -0
- package/app/components/tasks/ProjectSelect.vue +35 -0
- package/app/components/tasks/TaskCard.vue +182 -0
- package/app/components/tasks/TaskDetail.vue +160 -0
- package/app/components/tasks/TaskForm.vue +280 -0
- package/app/components/tasks/TaskList.vue +69 -0
- package/app/components/view/ViewToc.vue +85 -0
- package/app/composables/useAgents.ts +153 -0
- package/app/composables/useAuth.ts +73 -0
- package/app/composables/useChat.ts +298 -0
- package/app/composables/useDocument.ts +141 -0
- package/app/composables/useEditor.ts +100 -0
- package/app/composables/useFileTree.ts +220 -0
- package/app/composables/useHookEvents.ts +68 -0
- package/app/composables/useMemories.ts +83 -0
- package/app/composables/useNotificationBus.ts +154 -0
- package/app/composables/usePreferences.ts +131 -0
- package/app/composables/useProjects.ts +97 -0
- package/app/composables/useSearch.ts +52 -0
- package/app/composables/useTasks.ts +201 -0
- package/app/composables/useTerminal.ts +135 -0
- package/app/layouts/auth.vue +20 -0
- package/app/layouts/dashboard.vue +186 -0
- package/app/layouts/view.vue +60 -0
- package/app/middleware/auth.ts +9 -0
- package/app/pages/agents/[id].vue +602 -0
- package/app/pages/agents/index.vue +412 -0
- package/app/pages/chat.vue +146 -0
- package/app/pages/dashboard.vue +80 -0
- package/app/pages/docs.vue +131 -0
- package/app/pages/hooks.vue +163 -0
- package/app/pages/index.vue +249 -0
- package/app/pages/login.vue +60 -0
- package/app/pages/memories.vue +282 -0
- package/app/pages/settings.vue +625 -0
- package/app/pages/tasks.vue +312 -0
- package/app/pages/view/[uuid].vue +376 -0
- package/dist/cli/index.js +2711 -0
- package/drizzle.config.ts +10 -0
- package/nuxt.config.ts +98 -0
- package/package.json +107 -0
- package/server/api/agents/[id]/cancel.post.ts +27 -0
- package/server/api/agents/[id]/run.post.ts +34 -0
- package/server/api/agents/[id]/runs.get.ts +45 -0
- package/server/api/agents/[id]/stats.get.ts +94 -0
- package/server/api/agents/[id].delete.ts +29 -0
- package/server/api/agents/[id].get.ts +25 -0
- package/server/api/agents/[id].patch.ts +55 -0
- package/server/api/agents/index.get.ts +15 -0
- package/server/api/agents/index.post.ts +48 -0
- package/server/api/agents/stats.get.ts +86 -0
- package/server/api/auth/[...all].ts +5 -0
- package/server/api/conversations/[id].delete.ts +16 -0
- package/server/api/conversations/[id].get.ts +34 -0
- package/server/api/conversations/index.get.ts +17 -0
- package/server/api/documents/[id]/index.delete.ts +47 -0
- package/server/api/documents/[id]/index.put.ts +102 -0
- package/server/api/documents/[id]/public.get.ts +60 -0
- package/server/api/documents/[id]/restore.post.ts +65 -0
- package/server/api/documents/by-path.post.ts +168 -0
- package/server/api/documents/index.get.ts +48 -0
- package/server/api/fs/delete.post.ts +41 -0
- package/server/api/fs/list.get.ts +99 -0
- package/server/api/fs/mkdir.post.ts +44 -0
- package/server/api/fs/move.post.ts +68 -0
- package/server/api/fs/read.post.ts +48 -0
- package/server/api/fs/rename.post.ts +55 -0
- package/server/api/fs/write.post.ts +51 -0
- package/server/api/health.get.ts +40 -0
- package/server/api/home.get.ts +26 -0
- package/server/api/hooks/events/index.get.ts +56 -0
- package/server/api/hooks/events/index.post.ts +36 -0
- package/server/api/hooks/stats.get.ts +99 -0
- package/server/api/memory/[id].delete.ts +26 -0
- package/server/api/memory/context.get.ts +83 -0
- package/server/api/memory/extract.post.ts +42 -0
- package/server/api/memory/search.get.ts +70 -0
- package/server/api/memory/store.post.ts +31 -0
- package/server/api/projects/[id]/index.delete.ts +40 -0
- package/server/api/projects/[id]/index.get.ts +25 -0
- package/server/api/projects/[id]/index.put.ts +50 -0
- package/server/api/projects/index.get.ts +20 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/secrets/[key].delete.ts +31 -0
- package/server/api/secrets/[key].get.ts +30 -0
- package/server/api/secrets/[key].put.ts +52 -0
- package/server/api/secrets/index.get.ts +20 -0
- package/server/api/secrets/index.post.ts +58 -0
- package/server/api/tasks/[id]/index.delete.ts +46 -0
- package/server/api/tasks/[id]/index.get.ts +24 -0
- package/server/api/tasks/[id]/index.put.ts +70 -0
- package/server/api/tasks/[id]/restore.post.ts +49 -0
- package/server/api/tasks/index.get.ts +53 -0
- package/server/api/tasks/index.post.ts +47 -0
- package/server/api/tasks/tags.get.ts +21 -0
- package/server/api/user/email.patch.ts +56 -0
- package/server/db/index.ts +76 -0
- package/server/db/migrate.ts +41 -0
- package/server/db/schema.ts +345 -0
- package/server/db/seed.ts +46 -0
- package/server/db/types.ts +28 -0
- package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
- package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
- package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
- package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
- package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
- package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
- package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
- package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
- package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
- package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
- package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
- package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
- package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
- package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
- package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
- package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
- package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
- package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
- package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
- package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
- package/server/drizzle/migrations/meta/_journal.json +76 -0
- package/server/middleware/auth.ts +79 -0
- package/server/plugins/00.env-validate.ts +38 -0
- package/server/plugins/01.api-token.ts +31 -0
- package/server/plugins/02.database.ts +54 -0
- package/server/plugins/03.file-watcher.ts +65 -0
- package/server/plugins/04.cron-agents.ts +26 -0
- package/server/routes/_ws/chat.ts +252 -0
- package/server/routes/notifications.ts +47 -0
- package/server/routes/terminal.ts +98 -0
- package/server/services/agent-executor.ts +218 -0
- package/server/services/cron-scheduler.ts +78 -0
- package/server/services/memory-extractor.ts +120 -0
- package/server/utils/agent-cleanup.ts +91 -0
- package/server/utils/agent-registry.ts +95 -0
- package/server/utils/auth.ts +33 -0
- package/server/utils/chat-session-manager.ts +59 -0
- package/server/utils/crypto.ts +40 -0
- package/server/utils/db-guard.ts +12 -0
- package/server/utils/db-state.ts +63 -0
- package/server/utils/document-sync.ts +207 -0
- package/server/utils/frontmatter.ts +84 -0
- package/server/utils/notification-bus.ts +60 -0
- package/server/utils/path-validator.ts +55 -0
- package/server/utils/pty-manager.ts +130 -0
- package/shared/types/index.ts +604 -0
- package/shared/utils/language-detection.ts +87 -0
- 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
|
+
})
|