cognova 0.1.0 → 0.1.1

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 (34) hide show
  1. package/app/app.vue +1 -1
  2. package/app/composables/useNotificationBus.ts +58 -42
  3. package/app/pages/settings.vue +147 -12
  4. package/package.json +1 -1
  5. package/server/api/agents/[id].delete.ts +3 -0
  6. package/server/api/agents/[id].patch.ts +3 -0
  7. package/server/api/agents/index.post.ts +3 -0
  8. package/server/api/conversations/[id].delete.ts +3 -0
  9. package/server/api/documents/[id]/index.delete.ts +3 -0
  10. package/server/api/documents/[id]/index.put.ts +3 -0
  11. package/server/api/documents/[id]/restore.post.ts +3 -0
  12. package/server/api/hooks/events/index.post.ts +3 -0
  13. package/server/api/memory/[id].delete.ts +3 -0
  14. package/server/api/memory/store.post.ts +3 -0
  15. package/server/api/projects/[id]/index.delete.ts +3 -0
  16. package/server/api/projects/[id]/index.put.ts +3 -0
  17. package/server/api/projects/index.post.ts +3 -0
  18. package/server/api/secrets/[key].delete.ts +3 -0
  19. package/server/api/secrets/[key].put.ts +3 -0
  20. package/server/api/secrets/index.post.ts +3 -0
  21. package/server/api/settings/index.get.ts +34 -0
  22. package/server/api/settings/index.put.ts +44 -0
  23. package/server/api/tasks/[id]/index.delete.ts +3 -0
  24. package/server/api/tasks/[id]/index.put.ts +3 -0
  25. package/server/api/tasks/[id]/restore.post.ts +3 -0
  26. package/server/api/tasks/index.post.ts +3 -0
  27. package/server/db/schema.ts +16 -0
  28. package/server/drizzle/migrations/0010_condemned_skrulls.sql +10 -0
  29. package/server/drizzle/migrations/meta/0010_snapshot.json +1556 -0
  30. package/server/drizzle/migrations/meta/_journal.json +7 -0
  31. package/server/services/agent-executor.ts +25 -26
  32. package/server/utils/notify-resource.ts +68 -0
  33. package/shared/types/index.ts +44 -13
  34. package/shared/utils/notification-defaults.ts +13 -0
package/app/app.vue CHANGED
@@ -29,7 +29,7 @@ useSeoMeta({
29
29
  </script>
30
30
 
31
31
  <template>
32
- <UApp>
32
+ <UApp :toaster="{ position: 'top-right' }">
33
33
  <NuxtLoadingIndicator />
34
34
 
35
35
  <NuxtLayout>
@@ -1,16 +1,30 @@
1
- import type { NotificationPayload } from '~~/shared/types'
1
+ import type { NotificationPayload, NotificationPreferences, NotificationAction } from '~~/shared/types'
2
+ import { defaultNotificationPreferences } from '~~/shared/utils/notification-defaults'
2
3
 
3
4
  export type NotificationBusStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
4
5
 
5
6
  // Shared state across all component instances
6
7
  const status = ref<NotificationBusStatus>('disconnected')
7
8
  const runningAgentIds = ref<Set<string>>(new Set())
9
+ const notificationPreferences = ref<NotificationPreferences>({ ...defaultNotificationPreferences })
8
10
  const ws = ref<WebSocket | null>(null)
9
11
  const reconnectAttempts = ref(0)
10
12
  const maxReconnectAttempts = 5
11
13
  const reconnectDelay = 2000
12
14
  let pingInterval: ReturnType<typeof setInterval> | null = null
13
15
  let isInitialized = false
16
+ let preferencesLoaded = false
17
+
18
+ const actionIcons: Record<NotificationAction, string> = {
19
+ create: 'i-lucide-plus-circle',
20
+ edit: 'i-lucide-pencil',
21
+ delete: 'i-lucide-trash-2',
22
+ restore: 'i-lucide-undo-2',
23
+ run: 'i-lucide-play',
24
+ cancel: 'i-lucide-x-circle',
25
+ complete: 'i-lucide-check-circle',
26
+ fail: 'i-lucide-alert-circle'
27
+ }
14
28
 
15
29
  export function useNotificationBus() {
16
30
  const toast = useToast()
@@ -21,43 +35,39 @@ export function useNotificationBus() {
21
35
  return `${protocol}//${window.location.host}/notifications`
22
36
  }
23
37
 
24
- function handleNotification(payload: NotificationPayload) {
25
- // Update running agents state
26
- if (payload.type === 'agent:started' && payload.agentId) {
27
- runningAgentIds.value.add(payload.agentId)
28
- } else if ((payload.type === 'agent:completed' || payload.type === 'agent:failed') && payload.agentId) {
29
- runningAgentIds.value.delete(payload.agentId)
38
+ async function loadPreferences() {
39
+ if (preferencesLoaded) return
40
+ try {
41
+ const response = await $fetch<{ data: { notifications: NotificationPreferences } }>('/api/settings')
42
+ notificationPreferences.value = { ...defaultNotificationPreferences, ...response.data.notifications }
43
+ preferencesLoaded = true
44
+ } catch {
45
+ console.warn('[notification-bus] Failed to load preferences, using defaults')
30
46
  }
47
+ }
31
48
 
32
- // Show toast for agent events
33
- if (payload.type === 'agent:started') {
34
- toast.add({
35
- title: 'Agent Started',
36
- description: payload.agentName || 'An agent has started running',
37
- icon: 'i-lucide-play',
38
- color: 'info'
39
- })
40
- } else if (payload.type === 'agent:completed') {
41
- toast.add({
42
- title: 'Agent Completed',
43
- description: payload.agentName || 'Agent run completed successfully',
44
- icon: 'i-lucide-check-circle',
45
- color: 'success'
46
- })
47
- } else if (payload.type === 'agent:failed') {
48
- toast.add({
49
- title: 'Agent Failed',
50
- description: payload.message || payload.agentName || 'Agent run failed',
51
- icon: 'i-lucide-alert-circle',
52
- color: 'error'
53
- })
54
- } else if (payload.type === 'toast' && payload.title) {
55
- toast.add({
56
- title: payload.title,
57
- description: payload.message,
58
- color: payload.color || 'info'
59
- })
49
+ function handleNotification(payload: NotificationPayload) {
50
+ // Always track running agents regardless of preferences
51
+ if (payload.resource === 'agent' && payload.resourceId) {
52
+ if (payload.action === 'run')
53
+ runningAgentIds.value.add(payload.resourceId)
54
+ else if (payload.action === 'complete' || payload.action === 'fail' || payload.action === 'cancel')
55
+ runningAgentIds.value.delete(payload.resourceId)
60
56
  }
57
+
58
+ // Check resource-level preference
59
+ const pref = notificationPreferences.value[payload.resource]
60
+ if (!pref?.enabled) return
61
+
62
+ // Check subtype preference (defaults to enabled if not explicitly set)
63
+ if (pref.subtypes && pref.subtypes[payload.action] === false) return
64
+
65
+ toast.add({
66
+ title: payload.title || 'Notification',
67
+ description: payload.message,
68
+ icon: actionIcons[payload.action] || 'i-lucide-bell',
69
+ color: payload.color || 'info'
70
+ })
61
71
  }
62
72
 
63
73
  function connect() {
@@ -75,6 +85,7 @@ export function useNotificationBus() {
75
85
  status.value = 'connected'
76
86
  reconnectAttempts.value = 0
77
87
  console.log('[notification-bus] Connected')
88
+ loadPreferences()
78
89
  startPingInterval()
79
90
  }
80
91
 
@@ -83,13 +94,12 @@ export function useNotificationBus() {
83
94
  const data = JSON.parse(event.data) as Record<string, unknown>
84
95
  const msgType = data.type as string
85
96
 
86
- // Skip pong and connected messages
97
+ // Skip control messages
87
98
  if (msgType === 'pong' || msgType === 'connected') return
88
99
 
89
- // Only handle known notification types
90
- if (msgType === 'agent:started' || msgType === 'agent:completed' || msgType === 'agent:failed' || msgType === 'toast') {
100
+ // Handle resource change notifications
101
+ if (msgType === 'resource_change')
91
102
  handleNotification(data as unknown as NotificationPayload)
92
- }
93
103
  } catch (e) {
94
104
  console.error('[notification-bus] Failed to parse message:', e)
95
105
  }
@@ -123,9 +133,8 @@ export function useNotificationBus() {
123
133
  }
124
134
 
125
135
  function sendPing() {
126
- if (ws.value?.readyState === WebSocket.OPEN) {
136
+ if (ws.value?.readyState === WebSocket.OPEN)
127
137
  ws.value.send(JSON.stringify({ type: 'ping' }))
128
- }
129
138
  }
130
139
 
131
140
  function startPingInterval() {
@@ -144,11 +153,18 @@ export function useNotificationBus() {
144
153
  return runningAgentIds.value.has(agentId)
145
154
  }
146
155
 
156
+ function updatePreferences(prefs: NotificationPreferences) {
157
+ notificationPreferences.value = prefs
158
+ }
159
+
147
160
  return {
148
161
  status: readonly(status),
149
162
  runningAgentIds: readonly(runningAgentIds),
163
+ notificationPreferences: readonly(notificationPreferences),
150
164
  isAgentRunning,
151
165
  connect,
152
- disconnect
166
+ disconnect,
167
+ loadPreferences,
168
+ updatePreferences
153
169
  }
154
170
  }
@@ -1,4 +1,7 @@
1
1
  <script setup lang="ts">
2
+ import type { NotificationPreferences, NotificationResource, NotificationAction } from '~~/shared/types'
3
+ import { defaultNotificationPreferences } from '~~/shared/utils/notification-defaults'
4
+
2
5
  interface Secret {
3
6
  id: string
4
7
  key: string
@@ -14,6 +17,7 @@ definePageMeta({
14
17
 
15
18
  const { user, updateProfile, changeEmail, changePassword } = useAuth()
16
19
  const toast = useToast()
20
+ const { updatePreferences } = useNotificationBus()
17
21
 
18
22
  // Profile form state
19
23
  const profileState = reactive({
@@ -190,9 +194,73 @@ function formatDate(dateStr: string) {
190
194
  })
191
195
  }
192
196
 
193
- // Load secrets when component mounts
197
+ // === Notification Preferences ===
198
+ const notifPrefs = ref<NotificationPreferences>({ ...defaultNotificationPreferences })
199
+ const notifLoading = ref(false)
200
+ const notifSaving = ref(false)
201
+ const expandedResources = ref<Set<string>>(new Set())
202
+
203
+ const resourceConfig: { key: NotificationResource, label: string, icon: string, subtypes: NotificationAction[] }[] = [
204
+ { key: 'task', label: 'Tasks', icon: 'i-lucide-check-square', subtypes: ['create', 'edit', 'delete', 'restore'] },
205
+ { key: 'project', label: 'Projects', icon: 'i-lucide-folder', subtypes: ['create', 'edit', 'delete'] },
206
+ { key: 'agent', label: 'Agents', icon: 'i-lucide-bot', subtypes: ['create', 'edit', 'delete', 'run', 'complete', 'fail', 'cancel'] },
207
+ { key: 'document', label: 'Documents', icon: 'i-lucide-file-text', subtypes: ['edit', 'delete', 'restore'] },
208
+ { key: 'memory', label: 'Memories', icon: 'i-lucide-brain', subtypes: ['create', 'delete'] },
209
+ { key: 'reminder', label: 'Reminders', icon: 'i-lucide-bell', subtypes: ['create'] },
210
+ { key: 'secret', label: 'Secrets', icon: 'i-lucide-key-round', subtypes: ['create', 'edit', 'delete'] },
211
+ { key: 'hook', label: 'Hooks', icon: 'i-lucide-webhook', subtypes: ['create'] },
212
+ { key: 'conversation', label: 'Conversations', icon: 'i-lucide-message-square', subtypes: ['delete'] }
213
+ ]
214
+
215
+ async function loadNotificationPrefs() {
216
+ notifLoading.value = true
217
+ try {
218
+ const { data } = await $fetch<{ data: { notifications: NotificationPreferences } }>('/api/settings')
219
+ notifPrefs.value = { ...defaultNotificationPreferences, ...data.notifications }
220
+ } catch {
221
+ notifPrefs.value = { ...defaultNotificationPreferences }
222
+ }
223
+ notifLoading.value = false
224
+ }
225
+
226
+ async function saveNotificationPrefs() {
227
+ notifSaving.value = true
228
+ try {
229
+ await $fetch('/api/settings', {
230
+ method: 'PUT',
231
+ body: { notifications: notifPrefs.value }
232
+ })
233
+ updatePreferences(notifPrefs.value)
234
+ toast.add({ title: 'Notification preferences saved', color: 'success' })
235
+ } catch {
236
+ toast.add({ title: 'Failed to save preferences', color: 'error' })
237
+ }
238
+ notifSaving.value = false
239
+ }
240
+
241
+ function toggleResourceExpand(key: string) {
242
+ if (expandedResources.value.has(key))
243
+ expandedResources.value.delete(key)
244
+ else
245
+ expandedResources.value.add(key)
246
+ }
247
+
248
+ function setResourceEnabled(key: NotificationResource, enabled: boolean) {
249
+ notifPrefs.value[key] = { ...notifPrefs.value[key], enabled }
250
+ }
251
+
252
+ function setSubtypeEnabled(key: NotificationResource, subtype: NotificationAction, enabled: boolean) {
253
+ const current = notifPrefs.value[key]
254
+ notifPrefs.value[key] = {
255
+ ...current,
256
+ subtypes: { ...current?.subtypes, [subtype]: enabled }
257
+ }
258
+ }
259
+
260
+ // Load secrets + notification prefs when component mounts
194
261
  onMounted(() => {
195
262
  fetchSecrets()
263
+ loadNotificationPrefs()
196
264
  })
197
265
 
198
266
  // Form handlers
@@ -498,17 +566,84 @@ async function handlePasswordSubmit() {
498
566
 
499
567
  <!-- App Tab -->
500
568
  <template #app>
501
- <div class="py-12 text-center">
502
- <UIcon
503
- name="i-lucide-settings"
504
- class="size-16 mx-auto mb-4 text-dimmed"
505
- />
506
- <h3 class="text-lg font-semibold mb-2">
507
- App Settings
508
- </h3>
509
- <p class="text-dimmed">
510
- Coming soon...
511
- </p>
569
+ <div class="max-w-2xl mx-auto py-6">
570
+ <div class="mb-6">
571
+ <h3 class="text-lg font-semibold mb-1">
572
+ Notification Preferences
573
+ </h3>
574
+ <p class="text-sm text-dimmed">
575
+ Choose which resource changes show toast notifications.
576
+ </p>
577
+ </div>
578
+
579
+ <div
580
+ v-if="notifLoading"
581
+ class="space-y-3"
582
+ >
583
+ <USkeleton
584
+ v-for="i in 5"
585
+ :key="i"
586
+ class="h-12 w-full"
587
+ />
588
+ </div>
589
+
590
+ <div
591
+ v-else
592
+ class="space-y-2"
593
+ >
594
+ <div
595
+ v-for="rc in resourceConfig"
596
+ :key="rc.key"
597
+ class="border border-default rounded-lg"
598
+ >
599
+ <div class="flex items-center justify-between px-4 py-3">
600
+ <div class="flex items-center gap-3">
601
+ <UButton
602
+ variant="ghost"
603
+ size="xs"
604
+ :icon="expandedResources.has(rc.key) ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
605
+ @click="toggleResourceExpand(rc.key)"
606
+ />
607
+ <UIcon
608
+ :name="rc.icon"
609
+ class="size-5 text-dimmed"
610
+ />
611
+ <span class="font-medium">{{ rc.label }}</span>
612
+ </div>
613
+ <USwitch
614
+ :model-value="notifPrefs[rc.key]?.enabled ?? false"
615
+ @update:model-value="(v: boolean) => setResourceEnabled(rc.key, v)"
616
+ />
617
+ </div>
618
+
619
+ <div
620
+ v-if="expandedResources.has(rc.key) && notifPrefs[rc.key]?.enabled"
621
+ class="border-t border-default px-4 py-3 space-y-2 bg-elevated/50"
622
+ >
623
+ <div
624
+ v-for="subtype in rc.subtypes"
625
+ :key="subtype"
626
+ class="flex items-center justify-between pl-11"
627
+ >
628
+ <span class="text-sm text-dimmed capitalize">{{ subtype }}</span>
629
+ <USwitch
630
+ :model-value="notifPrefs[rc.key]?.subtypes?.[subtype] !== false"
631
+ size="sm"
632
+ @update:model-value="(v: boolean) => setSubtypeEnabled(rc.key, subtype, v)"
633
+ />
634
+ </div>
635
+ </div>
636
+ </div>
637
+ </div>
638
+
639
+ <div class="mt-6">
640
+ <UButton
641
+ :loading="notifSaving"
642
+ @click="saveNotificationPrefs"
643
+ >
644
+ Save Preferences
645
+ </UButton>
646
+ </div>
512
647
  </div>
513
648
  </template>
514
649
  </UTabs>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cognova",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "description": "Personal knowledge management system with Claude Code integration",
6
6
  "repository": {
7
7
  "type": "git",
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
2
2
  import { getDb, schema } from '~~/server/db'
3
3
  import { requireDb } from '~~/server/utils/db-guard'
4
4
  import { unscheduleAgent } from '~~/server/services/cron-scheduler'
5
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
5
6
 
6
7
  export default defineEventHandler(async (event) => {
7
8
  requireDb(event)
@@ -25,5 +26,7 @@ export default defineEventHandler(async (event) => {
25
26
  throw createError({ statusCode: 404, message: 'Agent not found' })
26
27
  }
27
28
 
29
+ notifyResourceChange({ resource: 'agent', action: 'delete', resourceId: id, resourceName: deleted.name })
30
+
28
31
  return { data: deleted }
29
32
  })
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
2
2
  import { getDb, schema } from '~~/server/db'
3
3
  import { requireDb } from '~~/server/utils/db-guard'
4
4
  import { scheduleAgent, unscheduleAgent } from '~~/server/services/cron-scheduler'
5
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
5
6
 
6
7
  interface UpdateAgentBody {
7
8
  name?: string
@@ -51,5 +52,7 @@ export default defineEventHandler(async (event) => {
51
52
  }
52
53
  }
53
54
 
55
+ notifyResourceChange({ resource: 'agent', action: 'edit', resourceId: id, resourceName: agent!.name })
56
+
54
57
  return { data: agent }
55
58
  })
@@ -1,6 +1,7 @@
1
1
  import { getDb, schema } from '~~/server/db'
2
2
  import { requireDb } from '~~/server/utils/db-guard'
3
3
  import { scheduleAgent } from '~~/server/services/cron-scheduler'
4
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
4
5
 
5
6
  interface CreateAgentBody {
6
7
  name: string
@@ -44,5 +45,7 @@ export default defineEventHandler(async (event) => {
44
45
  scheduleAgent(agent!)
45
46
  }
46
47
 
48
+ notifyResourceChange({ resource: 'agent', action: 'create', resourceId: agent!.id, resourceName: agent!.name })
49
+
47
50
  return { data: agent }
48
51
  })
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
2
2
  import { getDb } from '~~/server/db'
3
3
  import * as schema from '~~/server/db/schema'
4
4
  import { requireDb } from '~~/server/utils/db-guard'
5
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
5
6
 
6
7
  export default defineEventHandler(async (event) => {
7
8
  requireDb(event)
@@ -12,5 +13,7 @@ export default defineEventHandler(async (event) => {
12
13
  await db.delete(schema.conversations)
13
14
  .where(eq(schema.conversations.id, id))
14
15
 
16
+ notifyResourceChange({ resource: 'conversation', action: 'delete', resourceId: id })
17
+
15
18
  return { data: { success: true } }
16
19
  })
@@ -4,6 +4,7 @@ import { getDb } from '~~/server/db'
4
4
  import * as schema from '~~/server/db/schema'
5
5
  import { requireDb } from '~~/server/utils/db-guard'
6
6
  import { validatePath } from '~~/server/utils/path-validator'
7
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
7
8
 
8
9
  export default defineEventHandler(async (event) => {
9
10
  requireDb(event)
@@ -43,5 +44,7 @@ export default defineEventHandler(async (event) => {
43
44
  modifiedBy: userId
44
45
  }).where(eq(schema.documents.id, id))
45
46
 
47
+ notifyResourceChange({ resource: 'document', action: 'delete', resourceId: id, resourceName: document.title })
48
+
46
49
  return { data: { id, deleted: true } }
47
50
  })
@@ -6,6 +6,7 @@ import * as schema from '~~/server/db/schema'
6
6
  import { requireDb } from '~~/server/utils/db-guard'
7
7
  import { validatePath } from '~~/server/utils/path-validator'
8
8
  import { parseFrontmatter, stringifyFrontmatter, computeContentHash, extractTitle } from '~~/server/utils/frontmatter'
9
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
9
10
 
10
11
  export default defineEventHandler(async (event) => {
11
12
  requireDb(event)
@@ -85,6 +86,8 @@ export default defineEventHandler(async (event) => {
85
86
  with: { project: true }
86
87
  })
87
88
 
89
+ notifyResourceChange({ resource: 'document', action: 'edit', resourceId: id, resourceName: updated?.title })
90
+
88
91
  return {
89
92
  data: {
90
93
  document: updated,
@@ -6,6 +6,7 @@ import * as schema from '~~/server/db/schema'
6
6
  import { requireDb } from '~~/server/utils/db-guard'
7
7
  import { validatePath } from '~~/server/utils/path-validator'
8
8
  import { stringifyFrontmatter } from '~~/server/utils/frontmatter'
9
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
9
10
 
10
11
  export default defineEventHandler(async (event) => {
11
12
  requireDb(event)
@@ -61,5 +62,7 @@ export default defineEventHandler(async (event) => {
61
62
  with: { project: true }
62
63
  })
63
64
 
65
+ notifyResourceChange({ resource: 'document', action: 'restore', resourceId: id, resourceName: restored?.title })
66
+
64
67
  return { data: restored }
65
68
  })
@@ -1,5 +1,6 @@
1
1
  import { getDb, schema } from '~~/server/db'
2
2
  import { requireDb } from '~~/server/utils/db-guard'
3
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
3
4
  import type { CreateHookEventInput } from '~~/shared/types'
4
5
 
5
6
  export default defineEventHandler(async (event) => {
@@ -32,5 +33,7 @@ export default defineEventHandler(async (event) => {
32
33
  if (!hookEvent)
33
34
  throw createError({ statusCode: 500, message: 'Failed to create hook event' })
34
35
 
36
+ notifyResourceChange({ resource: 'hook', action: 'create', resourceId: hookEvent!.id, resourceName: body.eventType })
37
+
35
38
  return { data: hookEvent }
36
39
  })
@@ -1,6 +1,7 @@
1
1
  import { eq } from 'drizzle-orm'
2
2
  import { getDb, schema } from '~~/server/db'
3
3
  import { requireDb } from '~~/server/utils/db-guard'
4
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
4
5
 
5
6
  export default defineEventHandler(async (event) => {
6
7
  requireDb(event)
@@ -22,5 +23,7 @@ export default defineEventHandler(async (event) => {
22
23
 
23
24
  await db.delete(schema.memoryChunks).where(eq(schema.memoryChunks.id, id))
24
25
 
26
+ notifyResourceChange({ resource: 'memory', action: 'delete', resourceId: id })
27
+
25
28
  return { data: { success: true } }
26
29
  })
@@ -1,5 +1,6 @@
1
1
  import { getDb, schema } from '~~/server/db'
2
2
  import { requireDb } from '~~/server/utils/db-guard'
3
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
3
4
  import type { CreateMemoryInput, MemoryChunk } from '~~/shared/types'
4
5
 
5
6
  export default defineEventHandler(async (event) => {
@@ -27,5 +28,7 @@ export default defineEventHandler(async (event) => {
27
28
 
28
29
  console.log(`[memory] Stored memory: ${body.chunkType} - ${body.content.slice(0, 50)}...`)
29
30
 
31
+ notifyResourceChange({ resource: 'memory', action: 'create', resourceId: inserted.id, resourceName: body.chunkType })
32
+
30
33
  return { data: inserted as MemoryChunk }
31
34
  })
@@ -1,6 +1,7 @@
1
1
  import { eq } from 'drizzle-orm'
2
2
  import { getDb, schema } from '~~/server/db'
3
3
  import { requireDb } from '~~/server/utils/db-guard'
4
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
4
5
 
5
6
  export default defineEventHandler(async (event) => {
6
7
  requireDb(event)
@@ -36,5 +37,7 @@ export default defineEventHandler(async (event) => {
36
37
  .where(eq(schema.projects.id, id))
37
38
  .returning()
38
39
 
40
+ notifyResourceChange({ resource: 'project', action: 'delete', resourceId: id, resourceName: project.name })
41
+
39
42
  return { data: project }
40
43
  })
@@ -1,6 +1,7 @@
1
1
  import { eq } from 'drizzle-orm'
2
2
  import { getDb, schema } from '~~/server/db'
3
3
  import { requireDb } from '~~/server/utils/db-guard'
4
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
4
5
  import type { UpdateProjectInput } from '~~/shared/types'
5
6
 
6
7
  export default defineEventHandler(async (event) => {
@@ -46,5 +47,7 @@ export default defineEventHandler(async (event) => {
46
47
  .where(eq(schema.projects.id, id))
47
48
  .returning()
48
49
 
50
+ notifyResourceChange({ resource: 'project', action: 'edit', resourceId: id, resourceName: project.name })
51
+
49
52
  return { data: project }
50
53
  })
@@ -1,5 +1,6 @@
1
1
  import { getDb, schema } from '~~/server/db'
2
2
  import { requireDb } from '~~/server/utils/db-guard'
3
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
3
4
  import type { CreateProjectInput } from '~~/shared/types'
4
5
 
5
6
  export default defineEventHandler(async (event) => {
@@ -30,5 +31,7 @@ export default defineEventHandler(async (event) => {
30
31
  })
31
32
  .returning()
32
33
 
34
+ notifyResourceChange({ resource: 'project', action: 'create', resourceId: project!.id, resourceName: project!.name })
35
+
33
36
  return { data: project }
34
37
  })
@@ -1,6 +1,7 @@
1
1
  import { getDb } from '~~/server/db'
2
2
  import { secrets } from '~~/server/db/schema'
3
3
  import { eq } from 'drizzle-orm'
4
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
4
5
 
5
6
  export default defineEventHandler(async (event) => {
6
7
  const key = getRouterParam(event, 'key')
@@ -27,5 +28,7 @@ export default defineEventHandler(async (event) => {
27
28
 
28
29
  await db.delete(secrets).where(eq(secrets.key, key))
29
30
 
31
+ notifyResourceChange({ resource: 'secret', action: 'delete', resourceName: key })
32
+
30
33
  return { data: { deleted: true } }
31
34
  })
@@ -1,6 +1,7 @@
1
1
  import { getDb } from '~~/server/db'
2
2
  import { secrets } from '~~/server/db/schema'
3
3
  import { encryptSecret } from '~~/server/utils/crypto'
4
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
4
5
  import { eq } from 'drizzle-orm'
5
6
 
6
7
  interface UpdateSecretInput {
@@ -48,5 +49,7 @@ export default defineEventHandler(async (event) => {
48
49
  updatedAt: secrets.updatedAt
49
50
  })
50
51
 
52
+ notifyResourceChange({ resource: 'secret', action: 'edit', resourceId: result!.id, resourceName: result!.key })
53
+
51
54
  return { data: result }
52
55
  })
@@ -1,6 +1,7 @@
1
1
  import { getDb } from '~~/server/db'
2
2
  import { secrets } from '~~/server/db/schema'
3
3
  import { encryptSecret } from '~~/server/utils/crypto'
4
+ import { notifyResourceChange } from '~~/server/utils/notify-resource'
4
5
 
5
6
  interface CreateSecretInput {
6
7
  key: string
@@ -54,5 +55,7 @@ export default defineEventHandler(async (event) => {
54
55
  createdAt: secrets.createdAt
55
56
  })
56
57
 
58
+ notifyResourceChange({ resource: 'secret', action: 'create', resourceId: result!.id, resourceName: result!.key })
59
+
57
60
  return { data: result }
58
61
  })
@@ -0,0 +1,34 @@
1
+ import { eq } from 'drizzle-orm'
2
+ import { getDb, schema } from '~~/server/db'
3
+ import { requireDb } from '~~/server/utils/db-guard'
4
+ import { defaultNotificationPreferences } from '~~/shared/utils/notification-defaults'
5
+ import type { UserSettings } from '~~/shared/types'
6
+
7
+ export default defineEventHandler(async (event) => {
8
+ requireDb(event)
9
+
10
+ const userId = event.context.user?.id
11
+ if (!userId)
12
+ throw createError({ statusCode: 401, message: 'Unauthorized' })
13
+
14
+ const db = getDb()
15
+
16
+ const existing = await db.query.userSettings.findFirst({
17
+ where: eq(schema.userSettings.userId, userId)
18
+ })
19
+
20
+ if (!existing)
21
+ return { data: { notifications: defaultNotificationPreferences } as UserSettings }
22
+
23
+ const settings = JSON.parse(existing.settings) as Partial<UserSettings>
24
+
25
+ // Merge with defaults so new resource types are always present
26
+ return {
27
+ data: {
28
+ notifications: {
29
+ ...defaultNotificationPreferences,
30
+ ...settings.notifications
31
+ }
32
+ } as UserSettings
33
+ }
34
+ })