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,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
|
+
})
|