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,76 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1769553197688,
9
+ "tag": "0000_brown_george_stacy",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1769561551712,
16
+ "tag": "0001_stormy_pyro",
17
+ "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1769565938491,
23
+ "tag": "0002_clean_colossus",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "7",
29
+ "when": 1769566836364,
30
+ "tag": "0003_fine_joystick",
31
+ "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1769597683643,
37
+ "tag": "0004_tan_groot",
38
+ "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "7",
43
+ "when": 1769657793143,
44
+ "tag": "0005_cloudy_lilith",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "7",
50
+ "when": 1769741983377,
51
+ "tag": "0006_ordinary_retro_girl",
52
+ "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "7",
57
+ "when": 1769796171417,
58
+ "tag": "0007_flowery_venus",
59
+ "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "7",
64
+ "when": 1769837309085,
65
+ "tag": "0008_talented_zombie",
66
+ "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "7",
71
+ "when": 1771286195501,
72
+ "tag": "0009_gray_shen",
73
+ "breakpoints": true
74
+ }
75
+ ]
76
+ }
@@ -0,0 +1,79 @@
1
+ import type { H3Event } from 'h3'
2
+ import { auth } from '~~/server/utils/auth'
3
+ import { getDb } from '~~/server/db'
4
+
5
+ // Paths that don't require authentication
6
+ const publicPaths = [
7
+ '/api/auth', // BetterAuth endpoints
8
+ '/api/health', // Health check
9
+ '/api/home', // Public home page content
10
+ '/api/_mdc', // MDC syntax highlighting
11
+ '/_nuxt', // Nuxt assets
12
+ '/login', // Login page
13
+ '/view' // Public document viewer
14
+ ]
15
+
16
+ // Check for API token authentication (for CLI tools)
17
+ async function checkApiToken(event: H3Event): Promise<boolean> {
18
+ const apiToken = process.env.COGNOVA_API_TOKEN
19
+ if (!apiToken) return false
20
+
21
+ const headerToken = getHeader(event, 'X-API-Token')
22
+ if (!headerToken || headerToken !== apiToken) return false
23
+
24
+ // Token matches - get the first user as the authenticated user for CLI
25
+ const db = getDb()
26
+ const user = await db.query.user.findFirst()
27
+ if (user) {
28
+ event.context.user = user
29
+ event.context.session = { id: 'api-token', userId: user.id }
30
+ }
31
+ return true
32
+ }
33
+
34
+ export default defineEventHandler(async (event) => {
35
+ const path = getRequestURL(event).pathname
36
+
37
+ // Skip auth for root path (public home page)
38
+ if (path === '/') return
39
+
40
+ // Skip auth for public paths
41
+ if (publicPaths.some(p => path.startsWith(p))) return
42
+
43
+ // Skip auth for static assets
44
+ if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/)) return
45
+
46
+ // Check API token for CLI tools
47
+ if (await checkApiToken(event)) return
48
+
49
+ // Public document API - allow through but still try to get session for owner check
50
+ if (path.match(/^\/api\/documents\/[^/]+\/public$/)) {
51
+ const session = await auth.api.getSession({ headers: event.headers })
52
+ if (session) {
53
+ event.context.user = session.user
54
+ event.context.session = session.session
55
+ }
56
+ return
57
+ }
58
+
59
+ // Check session
60
+ const session = await auth.api.getSession({
61
+ headers: event.headers
62
+ })
63
+
64
+ if (!session) {
65
+ // API requests get 401
66
+ if (path.startsWith('/api/')) {
67
+ throw createError({
68
+ statusCode: 401,
69
+ message: 'Unauthorized'
70
+ })
71
+ }
72
+ // Page requests redirect to login
73
+ return sendRedirect(event, '/login')
74
+ }
75
+
76
+ // Attach user to event context for use in routes
77
+ event.context.user = session.user
78
+ event.context.session = session.session
79
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Validates required environment variables on server startup.
3
+ * Runs before all other plugins to catch misconfigurations early.
4
+ */
5
+ export default defineNitroPlugin(() => {
6
+ const warnings: string[] = []
7
+ const errors: string[] = []
8
+
9
+ // Required for auth to function
10
+ if (!process.env.BETTER_AUTH_SECRET) {
11
+ errors.push('BETTER_AUTH_SECRET is not set. Auth will not work. Generate one with: openssl rand -base64 32')
12
+ }
13
+
14
+ // Required for file operations
15
+ if (!process.env.VAULT_PATH) {
16
+ errors.push('VAULT_PATH is not set. File browsing and document features will not work.')
17
+ }
18
+
19
+ // Optional but important
20
+ if (!process.env.DATABASE_URL) {
21
+ warnings.push('DATABASE_URL is not set. Database features (tasks, agents, conversations, memory) will be disabled.')
22
+ }
23
+
24
+ if (!process.env.BETTER_AUTH_URL) {
25
+ warnings.push('BETTER_AUTH_URL is not set. Auth callbacks may not work correctly. Set to your app URL (e.g. http://localhost:3000).')
26
+ }
27
+
28
+ for (const w of warnings)
29
+ console.warn(`[env] WARNING: ${w}`)
30
+
31
+ if (errors.length > 0) {
32
+ for (const e of errors)
33
+ console.error(`[env] ERROR: ${e}`)
34
+
35
+ console.error('[env] Fix the above errors in your .env file and restart.')
36
+ console.error('[env] See .env.example for reference.')
37
+ }
38
+ })
@@ -0,0 +1,31 @@
1
+ import { randomBytes } from 'crypto'
2
+ import { writeFileSync } from 'fs'
3
+ import { join } from 'path'
4
+
5
+ /**
6
+ * Generates an API token on server startup for CLI tools.
7
+ * The token is written to .api-token file in the project root
8
+ * and set in the runtime environment.
9
+ */
10
+ export default defineNitroPlugin(() => {
11
+ // Skip if token already set via environment variable
12
+ if (process.env.COGNOVA_API_TOKEN) {
13
+ console.log('[api-token] Using existing COGNOVA_API_TOKEN from environment')
14
+ return
15
+ }
16
+
17
+ // Generate a secure random token
18
+ const token = randomBytes(32).toString('hex')
19
+
20
+ // Set in process environment for auth middleware
21
+ process.env.COGNOVA_API_TOKEN = token
22
+
23
+ // Write to file for CLI tools to read
24
+ const tokenPath = join(process.cwd(), '.api-token')
25
+ try {
26
+ writeFileSync(tokenPath, token, { mode: 0o600 }) // Read/write only for owner
27
+ console.log(`[api-token] Generated API token, written to ${tokenPath}`)
28
+ } catch (err) {
29
+ console.error('[api-token] Failed to write token file:', err)
30
+ }
31
+ })
@@ -0,0 +1,54 @@
1
+ import { runMigrations } from '~~/server/db/migrate'
2
+ import { warmupDb } from '~~/server/db'
3
+ import { seedIfEmpty } from '~~/server/db/seed'
4
+ import { setDbState } from '~~/server/utils/db-state'
5
+
6
+ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
7
+ return Promise.race([
8
+ promise,
9
+ new Promise<never>((_, reject) =>
10
+ setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
11
+ )
12
+ ])
13
+ }
14
+
15
+ export default defineNitroPlugin(async () => {
16
+ if (!process.env.DATABASE_URL) {
17
+ console.warn('[db] DATABASE_URL not set, database features disabled')
18
+ setDbState(false, 'DATABASE_URL not configured')
19
+ return
20
+ }
21
+
22
+ try {
23
+ // In development, skip migrations - use db:push instead
24
+ // In production, run migrations on startup
25
+ const skipMigrations = process.env.NODE_ENV !== 'production' && process.env.DB_SKIP_MIGRATIONS !== 'false'
26
+
27
+ if (skipMigrations) {
28
+ console.log('[db] Skipping migrations in development (use db:push for schema changes)')
29
+ } else {
30
+ try {
31
+ await withTimeout(runMigrations(), 30000, 'Migration')
32
+ } catch (migrationError) {
33
+ // Don't let migration failure prevent DB from being usable
34
+ console.error('[db] Migration issue (continuing anyway):', migrationError instanceof Error ? migrationError.message : migrationError)
35
+ }
36
+ }
37
+
38
+ await withTimeout(warmupDb(), 15000, 'DB warmup')
39
+
40
+ // Seed default user if database is empty
41
+ await seedIfEmpty()
42
+ } catch (error) {
43
+ console.error('[db] Database initialization failed:', error)
44
+
45
+ // In production, don't crash - just disable DB features
46
+ if (process.env.NODE_ENV === 'production') {
47
+ console.error('[db] Continuing with database features disabled')
48
+ setDbState(false, error instanceof Error ? error.message : 'Initialization failed')
49
+ } else {
50
+ // In development, fail fast
51
+ throw error
52
+ }
53
+ }
54
+ })
@@ -0,0 +1,65 @@
1
+ import { watch } from 'chokidar'
2
+ import { getVaultRoot } from '~~/server/utils/path-validator'
3
+ import { syncDocument, markDocumentDeleted, fullSync } from '~~/server/utils/document-sync'
4
+ import { waitForDb } from '~~/server/utils/db-state'
5
+
6
+ export default defineNitroPlugin(async (nitroApp) => {
7
+ // Wait for database to be initialized
8
+ const dbAvailable = await waitForDb()
9
+ if (!dbAvailable) {
10
+ console.log('[file-watcher] Database not available, skipping file watcher')
11
+ return
12
+ }
13
+
14
+ const vaultRoot = getVaultRoot()
15
+ console.log(`[file-watcher] Starting watcher on ${vaultRoot}`)
16
+
17
+ // Full sync on startup
18
+ try {
19
+ const stats = await fullSync()
20
+ if (stats.added + stats.removed + stats.updated > 0)
21
+ console.log(`[file-watcher] Initial sync complete: ${stats.added} added, ${stats.removed} removed, ${stats.updated} updated`)
22
+ } catch (error) {
23
+ console.error('[file-watcher] Initial sync failed:', error)
24
+ }
25
+
26
+ // Debounce map for rapid changes
27
+ const pending = new Map<string, NodeJS.Timeout>()
28
+
29
+ function debounced(path: string, action: () => Promise<unknown>) {
30
+ const existing = pending.get(path)
31
+ if (existing) clearTimeout(existing)
32
+
33
+ pending.set(path, setTimeout(async () => {
34
+ pending.delete(path)
35
+ try {
36
+ await action()
37
+ } catch (error) {
38
+ console.error(`[file-watcher] Error processing ${path}:`, error)
39
+ }
40
+ }, 500))
41
+ }
42
+
43
+ // Start watching
44
+ const watcher = watch(vaultRoot, {
45
+ ignored: /(^|[/\\])\../, // Ignore dotfiles
46
+ persistent: true,
47
+ ignoreInitial: true,
48
+ awaitWriteFinish: {
49
+ stabilityThreshold: 500,
50
+ pollInterval: 100
51
+ }
52
+ })
53
+
54
+ watcher
55
+ .on('add', path => debounced(path, () => syncDocument(path)))
56
+ .on('change', path => debounced(path, () => syncDocument(path)))
57
+ .on('unlink', path => debounced(path, () => markDocumentDeleted(path)))
58
+ .on('error', error => console.error('[file-watcher] Error:', error))
59
+
60
+ // Cleanup on shutdown
61
+ nitroApp.hooks.hook('close', async () => {
62
+ await watcher.close()
63
+ console.log('[file-watcher] Watcher closed')
64
+ })
65
+ })
@@ -0,0 +1,26 @@
1
+ import { waitForDb } from '../utils/db-state'
2
+ import { initCronScheduler } from '../services/cron-scheduler'
3
+ import { cleanupOrphanedRuns } from '../utils/agent-cleanup'
4
+
5
+ export default defineNitroPlugin(async () => {
6
+ const dbAvailable = await waitForDb()
7
+ if (!dbAvailable) {
8
+ console.log('[cron-agents] Database not available, skipping scheduler')
9
+ return
10
+ }
11
+
12
+ try {
13
+ // Clean up any orphaned runs from previous server shutdown
14
+ const { cancelled, fixed } = await cleanupOrphanedRuns()
15
+ if (cancelled > 0 || fixed > 0) {
16
+ console.log(`[cron-agents] Cleanup: ${cancelled} orphaned runs cancelled, ${fixed} runs fixed`)
17
+ }
18
+
19
+ const count = await initCronScheduler()
20
+ if (count > 0) {
21
+ console.log(`[cron-agents] Initialized ${count} scheduled agents`)
22
+ }
23
+ } catch (error) {
24
+ console.error('[cron-agents] Failed to initialize scheduler:', error)
25
+ }
26
+ })
@@ -0,0 +1,252 @@
1
+ import { randomUUID } from 'crypto'
2
+ import { eq } from 'drizzle-orm'
3
+ import { chatSessionManager } from '~~/server/utils/chat-session-manager'
4
+ import { getDb } from '~~/server/db'
5
+ import * as schema from '~~/server/db/schema'
6
+ import type { ChatClientMessage, ChatContentBlock } from '~~/shared/types'
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ function send(peer: any, data: object) {
10
+ try {
11
+ const json = JSON.stringify(data)
12
+ peer.send(json)
13
+ } catch (err) {
14
+ console.error('[chat] send failed:', err)
15
+ }
16
+ }
17
+
18
+ export default defineWebSocketHandler({
19
+ open(peer) {
20
+ console.log(`[chat] WebSocket opened: ${peer.id}`)
21
+ send(peer, { type: 'chat:connected' })
22
+ },
23
+
24
+ message(peer, rawMessage) {
25
+ try {
26
+ const msg = JSON.parse(rawMessage.text()) as ChatClientMessage
27
+
28
+ switch (msg.type) {
29
+ case 'chat:send':
30
+ handleSend(peer, msg.message, msg.conversationId)
31
+ break
32
+ case 'chat:interrupt':
33
+ handleInterrupt(peer, msg.conversationId)
34
+ break
35
+ }
36
+ } catch (e) {
37
+ console.error('[chat] Message error:', e)
38
+ send(peer, { type: 'chat:error', message: 'Invalid message format' })
39
+ }
40
+ },
41
+
42
+ close(peer) {
43
+ console.log(`[chat] WebSocket closed: ${peer.id}`)
44
+ },
45
+
46
+ error(peer, error) {
47
+ console.error(`[chat] WebSocket error for ${peer.id}:`, error)
48
+ }
49
+ })
50
+
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ async function handleSend(peer: any, message: string, conversationId?: string) {
53
+ const db = getDb()
54
+
55
+ let convId = conversationId
56
+ let resumeSessionId: string | undefined
57
+
58
+ if (!convId) {
59
+ // New conversation
60
+ const [conv] = await db.insert(schema.conversations)
61
+ .values({
62
+ sessionId: randomUUID(),
63
+ title: message.slice(0, 100),
64
+ status: 'streaming',
65
+ messageCount: 0,
66
+ totalCostUsd: 0
67
+ })
68
+ .returning()
69
+
70
+ convId = conv!.id
71
+ send(peer, { type: 'chat:session_created', conversationId: convId })
72
+ } else {
73
+ // Continuing existing conversation — check for SDK session to resume
74
+ const existing = chatSessionManager.getSession(convId)
75
+ if (existing) resumeSessionId = existing.sdkSessionId
76
+
77
+ if (!resumeSessionId) {
78
+ const [conv] = await db.select()
79
+ .from(schema.conversations)
80
+ .where(eq(schema.conversations.id, convId))
81
+ .limit(1)
82
+
83
+ if (!conv) {
84
+ send(peer, { type: 'chat:error', message: 'Conversation not found' })
85
+ return
86
+ }
87
+ if (conv.sdkSessionId) resumeSessionId = conv.sdkSessionId
88
+ }
89
+
90
+ await db.update(schema.conversations)
91
+ .set({ status: 'streaming' })
92
+ .where(eq(schema.conversations.id, convId))
93
+ }
94
+
95
+ // Persist user message
96
+ await db.insert(schema.conversationMessages).values({
97
+ conversationId: convId,
98
+ role: 'user',
99
+ content: JSON.stringify([{ type: 'text', text: message }])
100
+ })
101
+
102
+ // Start SDK streaming (fire-and-forget so WS stays responsive for interrupts)
103
+ const session = chatSessionManager.startSession(convId, message, resumeSessionId)
104
+ send(peer, { type: 'chat:stream_start', conversationId: convId })
105
+
106
+ streamSDKResponse(peer, session, convId).catch((err) => {
107
+ console.error('[chat] Unhandled stream error:', err)
108
+ })
109
+ }
110
+
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ function handleInterrupt(peer: any, conversationId: string) {
113
+ const interrupted = chatSessionManager.interrupt(conversationId)
114
+ if (!interrupted)
115
+ send(peer, { type: 'chat:error', conversationId, message: 'No active session to interrupt' })
116
+ }
117
+
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ async function streamSDKResponse(peer: any, session: any, conversationId: string) {
120
+ const db = getDb()
121
+ const contentBlocks: ChatContentBlock[] = []
122
+ let currentText = ''
123
+
124
+ try {
125
+ for await (const message of session.conversation) {
126
+ if (session.interrupted) {
127
+ send(peer, { type: 'chat:interrupted', conversationId })
128
+ await db.update(schema.conversations)
129
+ .set({ status: 'interrupted' })
130
+ .where(eq(schema.conversations.id, conversationId))
131
+ break
132
+ }
133
+
134
+ if (message.type === 'system' && message.subtype === 'init') {
135
+ session.sdkSessionId = message.session_id
136
+ await db.update(schema.conversations)
137
+ .set({ sdkSessionId: message.session_id })
138
+ .where(eq(schema.conversations.id, conversationId))
139
+ } else if (message.type === 'stream_event') {
140
+ // Real-time streaming events from the Anthropic API
141
+ const event = message.event
142
+ if (event.type === 'content_block_delta') {
143
+ const deltaType = event.delta?.type
144
+ if (deltaType === 'text_delta' && event.delta.text) {
145
+ currentText += event.delta.text
146
+ send(peer, { type: 'chat:text_delta', conversationId, delta: event.delta.text })
147
+ }
148
+ } else if (event.type === 'content_block_start') {
149
+ if (event.content_block?.type === 'tool_use') {
150
+ send(peer, {
151
+ type: 'chat:tool_start',
152
+ conversationId,
153
+ toolUseId: event.content_block.id,
154
+ toolName: event.content_block.name
155
+ })
156
+ }
157
+ }
158
+ } else if (message.type === 'assistant') {
159
+ // Complete assistant turn — capture content blocks for persistence
160
+ for (const block of message.message?.content || []) {
161
+ if (block.type === 'text') {
162
+ contentBlocks.push({ type: 'text', text: block.text })
163
+ // Fallback: if no stream_events arrived, send full text now
164
+ if (!currentText)
165
+ send(peer, { type: 'chat:text_delta', conversationId, delta: block.text })
166
+ } else if (block.type === 'tool_use') {
167
+ contentBlocks.push({
168
+ type: 'tool_use',
169
+ id: block.id,
170
+ name: block.name,
171
+ input: block.input as Record<string, unknown>
172
+ })
173
+ }
174
+ }
175
+ currentText = ''
176
+ } else if (message.type === 'user') {
177
+ // User messages contain tool results from SDK tool execution
178
+ const content = message.message?.content
179
+ if (Array.isArray(content)) {
180
+ for (const block of content) {
181
+ if (block.type === 'tool_result') {
182
+ const resultText = typeof block.content === 'string'
183
+ ? block.content
184
+ : Array.isArray(block.content)
185
+ ? block.content.map((c: { text?: string }) => c.text || '').join('')
186
+ : JSON.stringify(block.content ?? '')
187
+ contentBlocks.push({
188
+ type: 'tool_result',
189
+ tool_use_id: block.tool_use_id,
190
+ content: resultText,
191
+ is_error: !!block.is_error
192
+ })
193
+ send(peer, {
194
+ type: 'chat:tool_end',
195
+ conversationId,
196
+ toolUseId: block.tool_use_id,
197
+ result: resultText.slice(0, 5000),
198
+ isError: !!block.is_error
199
+ })
200
+ }
201
+ }
202
+ }
203
+ } else if (message.type === 'result') {
204
+ const costUsd = message.total_cost_usd || 0
205
+ const durationMs = message.duration_ms || 0
206
+
207
+ // Persist assistant message
208
+ if (contentBlocks.length > 0) {
209
+ await db.insert(schema.conversationMessages).values({
210
+ conversationId,
211
+ role: 'assistant',
212
+ content: JSON.stringify(contentBlocks),
213
+ costUsd,
214
+ durationMs
215
+ })
216
+ }
217
+
218
+ // Count messages
219
+ const msgs = await db.select()
220
+ .from(schema.conversationMessages)
221
+ .where(eq(schema.conversationMessages.conversationId, conversationId))
222
+
223
+ // Update conversation metadata
224
+ await db.update(schema.conversations)
225
+ .set({
226
+ status: 'idle',
227
+ messageCount: msgs.length,
228
+ totalCostUsd: costUsd,
229
+ endedAt: new Date()
230
+ })
231
+ .where(eq(schema.conversations.id, conversationId))
232
+
233
+ send(peer, {
234
+ type: 'chat:stream_end',
235
+ conversationId,
236
+ costUsd,
237
+ durationMs
238
+ })
239
+ }
240
+ }
241
+ } catch (error) {
242
+ const errorMsg = error instanceof Error ? error.message : String(error)
243
+ console.error('[chat] Stream error:', errorMsg)
244
+ send(peer, { type: 'chat:error', conversationId, message: errorMsg })
245
+
246
+ await db.update(schema.conversations)
247
+ .set({ status: 'error' })
248
+ .where(eq(schema.conversations.id, conversationId))
249
+ } finally {
250
+ chatSessionManager.removeSession(conversationId)
251
+ }
252
+ }
@@ -0,0 +1,47 @@
1
+ import { notificationBus } from '../utils/notification-bus'
2
+
3
+ interface NotificationMessage {
4
+ type: 'ping' | 'subscribe'
5
+ }
6
+
7
+ export default defineWebSocketHandler({
8
+ open(peer) {
9
+ console.log(`Notification WebSocket opened: ${peer.id}`)
10
+
11
+ notificationBus.registerPeer({
12
+ id: peer.id,
13
+ send: (data: string) => peer.send(data)
14
+ })
15
+
16
+ // Send welcome message
17
+ peer.send(JSON.stringify({
18
+ type: 'connected',
19
+ message: 'Notification bus connected',
20
+ timestamp: new Date().toISOString()
21
+ }))
22
+ },
23
+
24
+ message(peer, message) {
25
+ try {
26
+ const msg = JSON.parse(message.text()) as NotificationMessage
27
+
28
+ switch (msg.type) {
29
+ case 'ping':
30
+ peer.send(JSON.stringify({ type: 'pong', timestamp: new Date().toISOString() }))
31
+ break
32
+ }
33
+ } catch (e) {
34
+ console.error('Notification message error:', e)
35
+ }
36
+ },
37
+
38
+ close(peer) {
39
+ console.log(`Notification WebSocket closed: ${peer.id}`)
40
+ notificationBus.unregisterPeer(peer.id)
41
+ },
42
+
43
+ error(peer, error) {
44
+ console.error(`Notification WebSocket error for ${peer.id}:`, error)
45
+ notificationBus.unregisterPeer(peer.id)
46
+ }
47
+ })