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,33 @@
1
+ import { betterAuth } from 'better-auth'
2
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle'
3
+ import { getDb, schema } from '../db'
4
+
5
+ export const auth = betterAuth({
6
+ baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
7
+ database: drizzleAdapter(getDb(), {
8
+ provider: 'pg',
9
+ schema: {
10
+ user: schema.user,
11
+ session: schema.session,
12
+ account: schema.account,
13
+ verification: schema.verification
14
+ }
15
+ }),
16
+ emailAndPassword: {
17
+ enabled: true
18
+ },
19
+ session: {
20
+ expiresIn: 60 * 60 * 24 * 7, // 7 days
21
+ updateAge: 60 * 60 * 24 // Update session every 24 hours
22
+ },
23
+ advanced: {
24
+ // For HTTP (non-HTTPS) access via IP/LAN, cookies must not require Secure flag
25
+ useSecureCookies: process.env.BETTER_AUTH_URL?.startsWith('https://') ?? false
26
+ },
27
+ trustedOrigins: process.env.ACCESS_MODE === 'any'
28
+ ? (request?: Request) => {
29
+ const origin = request?.headers.get('origin')
30
+ return origin ? [origin] : []
31
+ }
32
+ : process.env.BETTER_AUTH_URL ? [process.env.BETTER_AUTH_URL] : []
33
+ })
@@ -0,0 +1,59 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk'
2
+
3
+ interface ActiveSession {
4
+ conversationId: string
5
+ sdkSessionId: string
6
+ conversation: ReturnType<typeof query>
7
+ interrupted: boolean
8
+ startedAt: Date
9
+ }
10
+
11
+ class ChatSessionManager {
12
+ private sessions = new Map<string, ActiveSession>()
13
+
14
+ startSession(conversationId: string, prompt: string, resumeSessionId?: string): ActiveSession {
15
+ // Use the project directory as CWD so the SDK picks up .claude/ (skills, rules, CLAUDE.md)
16
+ // The vault is accessible via VAULT_PATH env var in tools
17
+ const projectDir = process.env.COGNOVA_PROJECT_DIR || process.cwd()
18
+ const conversation = query({
19
+ prompt,
20
+ options: {
21
+ cwd: projectDir,
22
+ settingSources: ['user', 'project'],
23
+ permissionMode: 'bypassPermissions',
24
+ allowDangerouslySkipPermissions: true,
25
+ maxTurns: 200,
26
+ includePartialMessages: true,
27
+ ...(resumeSessionId ? { resume: resumeSessionId } : {})
28
+ }
29
+ })
30
+
31
+ const session: ActiveSession = {
32
+ conversationId,
33
+ sdkSessionId: '',
34
+ conversation,
35
+ interrupted: false,
36
+ startedAt: new Date()
37
+ }
38
+
39
+ this.sessions.set(conversationId, session)
40
+ return session
41
+ }
42
+
43
+ getSession(conversationId: string): ActiveSession | undefined {
44
+ return this.sessions.get(conversationId)
45
+ }
46
+
47
+ interrupt(conversationId: string): boolean {
48
+ const session = this.sessions.get(conversationId)
49
+ if (!session) return false
50
+ session.interrupted = true
51
+ return true
52
+ }
53
+
54
+ removeSession(conversationId: string): void {
55
+ this.sessions.delete(conversationId)
56
+ }
57
+ }
58
+
59
+ export const chatSessionManager = new ChatSessionManager()
@@ -0,0 +1,40 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'
2
+
3
+ const ALGORITHM = 'aes-256-gcm'
4
+ const SALT = 'cognova-secrets-salt'
5
+
6
+ function deriveKey(): Buffer {
7
+ const secret = process.env.BETTER_AUTH_SECRET
8
+ if (!secret) throw new Error('BETTER_AUTH_SECRET not set')
9
+ return scryptSync(secret, SALT, 32)
10
+ }
11
+
12
+ export function encryptSecret(plaintext: string): { encrypted: string, iv: string } {
13
+ const key = deriveKey()
14
+ const iv = randomBytes(16)
15
+ const cipher = createCipheriv(ALGORITHM, key, iv)
16
+
17
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex')
18
+ encrypted += cipher.final('hex')
19
+ const authTag = cipher.getAuthTag().toString('hex')
20
+
21
+ return {
22
+ encrypted: encrypted + ':' + authTag,
23
+ iv: iv.toString('hex')
24
+ }
25
+ }
26
+
27
+ export function decryptSecret(encrypted: string, ivHex: string): string {
28
+ const key = deriveKey()
29
+ const iv = Buffer.from(ivHex, 'hex')
30
+ const [ciphertext, authTag] = encrypted.split(':')
31
+
32
+ const decipher = createDecipheriv(ALGORITHM, key, iv)
33
+ decipher.setAuthTag(Buffer.from(authTag!, 'hex'))
34
+
35
+ const decryptedBuf = Buffer.concat([
36
+ decipher.update(Buffer.from(ciphertext!, 'hex')),
37
+ decipher.final()
38
+ ])
39
+ return decryptedBuf.toString('utf8')
40
+ }
@@ -0,0 +1,12 @@
1
+ import type { H3Event } from 'h3'
2
+ import { isDbAvailable } from './db-state'
3
+
4
+ export function requireDb(_event: H3Event) {
5
+ if (!isDbAvailable()) {
6
+ throw createError({
7
+ statusCode: 503,
8
+ statusMessage: 'Database Unavailable',
9
+ message: 'Database features are currently disabled. Configure DATABASE_URL to enable.'
10
+ })
11
+ }
12
+ }
@@ -0,0 +1,63 @@
1
+ interface DbState {
2
+ available: boolean
3
+ error?: string
4
+ lastCheck: Date
5
+ }
6
+
7
+ let dbState: DbState = {
8
+ available: false,
9
+ lastCheck: new Date()
10
+ }
11
+
12
+ // Track whether setDbState has been called
13
+ let stateInitialized = false
14
+
15
+ // Promise that resolves when DB state is first set
16
+ let dbReadyResolve: ((available: boolean) => void) | null = null
17
+ let dbReadyPromise: Promise<boolean> | null = null
18
+
19
+ export function setDbState(available: boolean, error?: string) {
20
+ dbState = { available, error, lastCheck: new Date() }
21
+ stateInitialized = true
22
+
23
+ // Resolve the waiting promise if anyone is waiting
24
+ if (dbReadyResolve) {
25
+ dbReadyResolve(available)
26
+ dbReadyResolve = null
27
+ }
28
+ }
29
+
30
+ export function isDbAvailable(): boolean {
31
+ return dbState.available
32
+ }
33
+
34
+ export function getDbState(): DbState {
35
+ return { ...dbState }
36
+ }
37
+
38
+ /**
39
+ * Wait for the database to be initialized.
40
+ * Returns true if available, false if unavailable or timeout.
41
+ */
42
+ export function waitForDb(timeoutMs = 10000): Promise<boolean> {
43
+ // Already initialized
44
+ if (stateInitialized)
45
+ return Promise.resolve(dbState.available)
46
+
47
+ // Create promise if not already waiting
48
+ if (!dbReadyPromise) {
49
+ dbReadyPromise = new Promise<boolean>((resolve) => {
50
+ dbReadyResolve = resolve
51
+
52
+ // Timeout fallback
53
+ setTimeout(() => {
54
+ if (dbReadyResolve) {
55
+ dbReadyResolve(false)
56
+ dbReadyResolve = null
57
+ }
58
+ }, timeoutMs)
59
+ })
60
+ }
61
+
62
+ return dbReadyPromise
63
+ }
@@ -0,0 +1,207 @@
1
+ import { readFile, readdir, writeFile } from 'fs/promises'
2
+ import { join, basename } from 'path'
3
+ import { eq, isNull } from 'drizzle-orm'
4
+ import { getDb } from '~~/server/db'
5
+ import * as schema from '~~/server/db/schema'
6
+ import { getVaultRoot, toRelativePath } from './path-validator'
7
+ import { parseFrontmatter, stringifyFrontmatter, extractTitle, computeContentHash, isBinaryFile, getMimeType } from './frontmatter'
8
+ import { isDbAvailable } from './db-state'
9
+
10
+ // Default frontmatter values for new documents
11
+ const DEFAULT_FRONTMATTER = {
12
+ tags: [] as string[],
13
+ shared: false
14
+ }
15
+
16
+ export type SyncResult = 'added' | 'updated' | 'restored' | 'unchanged' | 'skipped'
17
+
18
+ export async function syncDocument(absolutePath: string): Promise<SyncResult> {
19
+ if (!isDbAvailable()) return 'skipped'
20
+
21
+ const db = getDb()
22
+ const relativePath = toRelativePath(absolutePath)
23
+ const filename = basename(absolutePath)
24
+
25
+ // Skip hidden files
26
+ if (filename.startsWith('.')) return 'skipped'
27
+
28
+ let fileContent: string
29
+ try {
30
+ fileContent = await readFile(absolutePath, 'utf-8')
31
+ } catch {
32
+ console.error(`[sync] Failed to read file: ${relativePath}`)
33
+ return 'skipped'
34
+ }
35
+
36
+ const isBinary = isBinaryFile(filename)
37
+
38
+ const [existing] = await db
39
+ .select()
40
+ .from(schema.documents)
41
+ .where(eq(schema.documents.path, relativePath))
42
+ .limit(1)
43
+
44
+ if (isBinary) {
45
+ if (!existing) {
46
+ await db.insert(schema.documents).values({
47
+ path: relativePath,
48
+ title: filename,
49
+ fileType: 'binary',
50
+ mimeType: getMimeType(filename),
51
+ syncedAt: new Date()
52
+ })
53
+ return 'added'
54
+ } else if (existing.deletedAt) {
55
+ // Restore previously deleted binary file
56
+ await db.update(schema.documents).set({
57
+ deletedAt: null,
58
+ syncedAt: new Date()
59
+ }).where(eq(schema.documents.id, existing.id))
60
+ return 'restored'
61
+ }
62
+ return 'unchanged'
63
+ }
64
+
65
+ const { metadata: initialMetadata, body: bodyContent } = parseFrontmatter(fileContent)
66
+ let metadata = initialMetadata
67
+ const title = extractTitle(metadata, bodyContent, filename)
68
+
69
+ // For new documents without frontmatter, add defaults
70
+ const needsDefaults = !existing && Object.keys(metadata).length === 0
71
+
72
+ if (needsDefaults) {
73
+ // Merge defaults into metadata
74
+ metadata = { ...DEFAULT_FRONTMATTER }
75
+
76
+ // Write frontmatter back to file
77
+ const newContent = stringifyFrontmatter(metadata, bodyContent)
78
+ try {
79
+ await writeFile(absolutePath, newContent, 'utf-8')
80
+ fileContent = newContent
81
+ } catch (err) {
82
+ console.error(`[sync] Failed to write frontmatter to ${relativePath}:`, err)
83
+ }
84
+ }
85
+
86
+ const contentHash = computeContentHash(fileContent)
87
+
88
+ if (existing) {
89
+ const contentChanged = existing.contentHash !== contentHash
90
+ const wasDeleted = existing.deletedAt !== null
91
+
92
+ // Update if content changed OR document was previously deleted (restore it)
93
+ if (contentChanged || wasDeleted) {
94
+ await db.update(schema.documents).set({
95
+ title,
96
+ content: bodyContent,
97
+ contentHash,
98
+ tags: Array.isArray(metadata.tags) ? metadata.tags as string[] : existing.tags,
99
+ deletedAt: null, // Clear deletion flag - file exists on disk
100
+ syncedAt: new Date(),
101
+ modifiedAt: new Date()
102
+ }).where(eq(schema.documents.id, existing.id))
103
+ return wasDeleted ? 'restored' : 'updated'
104
+ }
105
+ return 'unchanged'
106
+ }
107
+
108
+ await db.insert(schema.documents).values({
109
+ path: relativePath,
110
+ title,
111
+ content: bodyContent,
112
+ contentHash,
113
+ tags: Array.isArray(metadata.tags) ? metadata.tags as string[] : [],
114
+ shared: metadata.shared === true,
115
+ fileType: 'markdown',
116
+ syncedAt: new Date()
117
+ })
118
+ return 'added'
119
+ }
120
+
121
+ export async function markDocumentDeleted(absolutePath: string): Promise<void> {
122
+ if (!isDbAvailable()) return
123
+
124
+ const db = getDb()
125
+ const relativePath = toRelativePath(absolutePath)
126
+
127
+ await db.update(schema.documents).set({
128
+ deletedAt: new Date(),
129
+ modifiedAt: new Date()
130
+ }).where(eq(schema.documents.path, relativePath))
131
+ }
132
+
133
+ export async function handleDocumentMove(oldPath: string, newPath: string): Promise<void> {
134
+ if (!isDbAvailable()) return
135
+
136
+ const db = getDb()
137
+ const oldRelative = toRelativePath(oldPath)
138
+ const newRelative = toRelativePath(newPath)
139
+
140
+ await db.update(schema.documents).set({
141
+ path: newRelative,
142
+ modifiedAt: new Date()
143
+ }).where(eq(schema.documents.path, oldRelative))
144
+ }
145
+
146
+ export async function fullSync(): Promise<{ added: number, updated: number, removed: number }> {
147
+ if (!isDbAvailable()) {
148
+ console.log('[sync] Database not available for full sync')
149
+ return { added: 0, updated: 0, removed: 0 }
150
+ }
151
+
152
+ const db = getDb()
153
+ const vaultRoot = getVaultRoot()
154
+ const stats = { added: 0, updated: 0, removed: 0 }
155
+ const foundPaths = new Set<string>()
156
+
157
+ async function scanDir(dir: string): Promise<void> {
158
+ let entries
159
+ try {
160
+ entries = await readdir(dir, { withFileTypes: true })
161
+ } catch (err) {
162
+ console.error(`[sync] Failed to read directory ${dir}:`, err)
163
+ return
164
+ }
165
+
166
+ for (const entry of entries) {
167
+ if (entry.name.startsWith('.')) continue
168
+
169
+ const fullPath = join(dir, entry.name)
170
+
171
+ if (entry.isDirectory()) {
172
+ await scanDir(fullPath)
173
+ } else if (entry.isFile()) {
174
+ const relativePath = toRelativePath(fullPath)
175
+ foundPaths.add(relativePath)
176
+ try {
177
+ const result = await syncDocument(fullPath)
178
+ if (result === 'added') stats.added++
179
+ else if (result === 'updated' || result === 'restored') stats.updated++
180
+ } catch (err) {
181
+ console.error(`[sync] Failed to sync ${relativePath}:`, err)
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ console.log(`[sync] Starting full sync of ${vaultRoot}`)
188
+ await scanDir(vaultRoot)
189
+ console.log(`[sync] Scanned ${foundPaths.size} files`)
190
+
191
+ // Mark documents not found on disk as deleted
192
+ const dbDocs = await db
193
+ .select({ id: schema.documents.id, path: schema.documents.path })
194
+ .from(schema.documents)
195
+ .where(isNull(schema.documents.deletedAt))
196
+
197
+ for (const doc of dbDocs) {
198
+ if (!foundPaths.has(doc.path)) {
199
+ await db.update(schema.documents).set({
200
+ deletedAt: new Date()
201
+ }).where(eq(schema.documents.id, doc.id))
202
+ stats.removed++
203
+ }
204
+ }
205
+
206
+ return stats
207
+ }
@@ -0,0 +1,84 @@
1
+ import matter from 'gray-matter'
2
+ import { createHash } from 'crypto'
3
+
4
+ export interface ParsedDocument {
5
+ metadata: Record<string, unknown>
6
+ body: string
7
+ raw: string
8
+ }
9
+
10
+ export function parseFrontmatter(content: string): ParsedDocument {
11
+ const { data, content: body } = matter(content)
12
+ return {
13
+ metadata: data,
14
+ body,
15
+ raw: content
16
+ }
17
+ }
18
+
19
+ export function stringifyFrontmatter(metadata: Record<string, unknown>, body: string): string {
20
+ if (Object.keys(metadata).length === 0)
21
+ return body
22
+
23
+ return matter.stringify(body, metadata)
24
+ }
25
+
26
+ export function computeContentHash(content: string): string {
27
+ return createHash('sha256').update(content).digest('hex')
28
+ }
29
+
30
+ export function extractTitle(metadata: Record<string, unknown>, body: string, filename: string): string {
31
+ if (metadata.title && typeof metadata.title === 'string')
32
+ return metadata.title
33
+
34
+ const h1Match = body.match(/^#\s+(.+)$/m)
35
+ if (h1Match?.[1])
36
+ return h1Match[1].trim()
37
+
38
+ return filename.replace(/\.[^.]+$/, '')
39
+ }
40
+
41
+ const binaryExtensions = new Set([
42
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp', 'tiff',
43
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
44
+ 'zip', 'tar', 'gz', 'rar', '7z',
45
+ 'mp3', 'mp4', 'wav', 'avi', 'mov', 'mkv', 'flac',
46
+ 'ttf', 'otf', 'woff', 'woff2', 'eot'
47
+ ])
48
+
49
+ export function isBinaryFile(filename: string): boolean {
50
+ const ext = filename.split('.').pop()?.toLowerCase() || ''
51
+ return binaryExtensions.has(ext)
52
+ }
53
+
54
+ export function getFileType(filename: string): 'markdown' | 'text' | 'binary' {
55
+ if (isBinaryFile(filename)) return 'binary'
56
+ const ext = filename.split('.').pop()?.toLowerCase() || ''
57
+ if (ext === 'md' || ext === 'mdx') return 'markdown'
58
+ return 'text'
59
+ }
60
+
61
+ const mimeTypes: Record<string, string> = {
62
+ png: 'image/png',
63
+ jpg: 'image/jpeg',
64
+ jpeg: 'image/jpeg',
65
+ gif: 'image/gif',
66
+ webp: 'image/webp',
67
+ svg: 'image/svg+xml',
68
+ ico: 'image/x-icon',
69
+ bmp: 'image/bmp',
70
+ pdf: 'application/pdf',
71
+ doc: 'application/msword',
72
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
73
+ xls: 'application/vnd.ms-excel',
74
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
75
+ zip: 'application/zip',
76
+ mp3: 'audio/mpeg',
77
+ mp4: 'video/mp4',
78
+ wav: 'audio/wav'
79
+ }
80
+
81
+ export function getMimeType(filename: string): string | undefined {
82
+ const ext = filename.split('.').pop()?.toLowerCase()
83
+ return ext ? mimeTypes[ext] : undefined
84
+ }
@@ -0,0 +1,60 @@
1
+ import { EventEmitter } from 'events'
2
+ import type { NotificationPayload } from '~~/shared/types'
3
+
4
+ type Peer = {
5
+ id: string
6
+ send: (data: string) => void
7
+ }
8
+
9
+ class NotificationBus extends EventEmitter {
10
+ private peers = new Map<string, Peer>()
11
+
12
+ registerPeer(peer: Peer) {
13
+ this.peers.set(peer.id, peer)
14
+ console.log(`[notification-bus] Peer connected: ${peer.id} (${this.peers.size} total)`)
15
+ }
16
+
17
+ unregisterPeer(peerId: string) {
18
+ this.peers.delete(peerId)
19
+ console.log(`[notification-bus] Peer disconnected: ${peerId} (${this.peers.size} total)`)
20
+ }
21
+
22
+ broadcast(payload: NotificationPayload) {
23
+ const message = JSON.stringify({
24
+ ...payload,
25
+ timestamp: payload.timestamp || new Date().toISOString()
26
+ })
27
+
28
+ for (const [peerId, peer] of this.peers) {
29
+ try {
30
+ peer.send(message)
31
+ } catch {
32
+ // Peer may have disconnected, remove it
33
+ this.unregisterPeer(peerId)
34
+ }
35
+ }
36
+
37
+ console.log(`[notification-bus] Broadcast: ${payload.type} to ${this.peers.size} peers`)
38
+ }
39
+
40
+ sendToPeer(peerId: string, payload: NotificationPayload) {
41
+ const peer = this.peers.get(peerId)
42
+ if (peer) {
43
+ try {
44
+ peer.send(JSON.stringify({
45
+ ...payload,
46
+ timestamp: payload.timestamp || new Date().toISOString()
47
+ }))
48
+ } catch {
49
+ this.unregisterPeer(peerId)
50
+ }
51
+ }
52
+ }
53
+
54
+ getPeerCount(): number {
55
+ return this.peers.size
56
+ }
57
+ }
58
+
59
+ // Singleton instance
60
+ export const notificationBus = new NotificationBus()
@@ -0,0 +1,55 @@
1
+ import { resolve, relative, join } from 'path'
2
+ import { existsSync } from 'fs'
3
+
4
+ function getDefaultVaultPath(): string {
5
+ // Check common locations in order of preference
6
+ const candidates = [
7
+ process.env.VAULT_PATH,
8
+ '/tmp/cognova-vault',
9
+ process.env.HOME ? join(process.env.HOME, 'Documents', 'vault') : null,
10
+ process.cwd()
11
+ ].filter(Boolean) as string[]
12
+
13
+ for (const path of candidates) {
14
+ // Always resolve to absolute path
15
+ const absolutePath = resolve(path)
16
+ if (existsSync(absolutePath)) {
17
+ return absolutePath
18
+ }
19
+ }
20
+
21
+ // Fallback to current working directory
22
+ return process.cwd()
23
+ }
24
+
25
+ const VAULT_ROOT = getDefaultVaultPath()
26
+
27
+ export function getVaultRoot(): string {
28
+ return VAULT_ROOT
29
+ }
30
+
31
+ export function validatePath(requestedPath: string): string {
32
+ // Normalize the path - remove leading slash and resolve
33
+ const normalizedPath = requestedPath.replace(/^\/+/, '')
34
+ const resolved = resolve(VAULT_ROOT, normalizedPath)
35
+ const rel = relative(VAULT_ROOT, resolved)
36
+
37
+ // Prevent traversal outside vault
38
+ if (rel.startsWith('..') || !resolved.startsWith(VAULT_ROOT)) {
39
+ throw createError({
40
+ statusCode: 403,
41
+ message: 'Path outside vault directory'
42
+ })
43
+ }
44
+
45
+ return resolved
46
+ }
47
+
48
+ export function toRelativePath(absolutePath: string): string {
49
+ const rel = relative(VAULT_ROOT, absolutePath)
50
+ return '/' + rel
51
+ }
52
+
53
+ export function joinVaultPath(...paths: string[]): string {
54
+ return join(VAULT_ROOT, ...paths)
55
+ }