From 7752ba9ff9cee72b76f11c9cebc6f800572b110b Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 13 Apr 2026 23:00:01 +0200 Subject: [PATCH] feat(brain): add domain events + tools for finance, dreams, cards, times, events Extends the Companion Brain to 15 modules. Adds semantic domain events and LLM tools for finance, dreams, cards, times, and social events. New domain events (10 types): - Finance: TransactionCreated, TransactionDeleted - Dreams: DreamCreated, DreamDeleted - Cards: CardCreated, CardStudied - Times: TimerStarted, TimerStopped - Social Events: SocialEventCreated, SocialEventDeleted New tools (7 tools): - Finance: add_transaction - Dreams: create_dream - Cards: create_card - Times: start_timer, stop_timer - Events: create_social_event Totals: 45 event types, 32 tools across 15 modules. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/data/events/catalog.ts | 96 +++++++++++++++++++ apps/mana/apps/web/src/lib/data/tools/init.ts | 10 ++ .../lib/modules/cards/stores/cards.svelte.ts | 5 + .../apps/web/src/lib/modules/cards/tools.ts | 25 +++++ .../modules/dreams/stores/dreams.svelte.ts | 8 ++ .../apps/web/src/lib/modules/dreams/tools.ts | 27 ++++++ .../modules/events/stores/events.svelte.ts | 7 ++ .../apps/web/src/lib/modules/events/tools.ts | 29 ++++++ .../modules/finance/stores/finance.svelte.ts | 8 ++ .../apps/web/src/lib/modules/finance/tools.ts | 33 +++++++ .../lib/modules/times/stores/timer.svelte.ts | 11 +++ .../apps/web/src/lib/modules/times/tools.ts | 41 ++++++++ 12 files changed, 300 insertions(+) create mode 100644 apps/mana/apps/web/src/lib/modules/cards/tools.ts create mode 100644 apps/mana/apps/web/src/lib/modules/dreams/tools.ts create mode 100644 apps/mana/apps/web/src/lib/modules/events/tools.ts create mode 100644 apps/mana/apps/web/src/lib/modules/finance/tools.ts create mode 100644 apps/mana/apps/web/src/lib/modules/times/tools.ts diff --git a/apps/mana/apps/web/src/lib/data/events/catalog.ts b/apps/mana/apps/web/src/lib/data/events/catalog.ts index f2f9be0ad..4c778dca2 100644 --- a/apps/mana/apps/web/src/lib/data/events/catalog.ts +++ b/apps/mana/apps/web/src/lib/data/events/catalog.ts @@ -295,6 +295,82 @@ export interface ContactDeletedPayload { export type ContactsEventType = 'ContactCreated' | 'ContactDeleted'; +// ── Finance ───────────────────────────────────────── + +export interface TransactionCreatedPayload { + transactionId: string; + amount: number; + type: string; + category?: string; + description?: string; +} + +export interface TransactionDeletedPayload { + transactionId: string; +} + +export type FinanceEventType = 'TransactionCreated' | 'TransactionDeleted'; + +// ── Dreams ────────────────────────────────────────── + +export interface DreamCreatedPayload { + dreamId: string; + title?: string; + isLucid: boolean; + mood?: string; +} + +export interface DreamDeletedPayload { + dreamId: string; +} + +export type DreamsEventType = 'DreamCreated' | 'DreamDeleted'; + +// ── Cards ─────────────────────────────────────────── + +export interface CardStudiedPayload { + cardId: string; + deckId: string; + quality: number; +} + +export interface CardCreatedPayload { + cardId: string; + deckId: string; +} + +export type CardsEventType = 'CardStudied' | 'CardCreated'; + +// ── Times ─────────────────────────────────────────── + +export interface TimerStartedPayload { + entryId: string; + description?: string; + projectId?: string; +} + +export interface TimerStoppedPayload { + entryId: string; + durationMinutes: number; + description?: string; +} + +export type TimesEventType = 'TimerStarted' | 'TimerStopped'; + +// ── Social Events ─────────────────────────────────── + +export interface SocialEventCreatedPayload { + eventId: string; + title: string; + date?: string; +} + +export interface SocialEventDeletedPayload { + eventId: string; +} + +export type SocialEventsEventType = 'SocialEventCreated' | 'SocialEventDeleted'; + // ── Body ──────────────────────────────────────────── export interface WorkoutStartedPayload { @@ -369,6 +445,11 @@ export type ManaEventType = | JournalEventType | NotesEventType | ContactsEventType + | FinanceEventType + | DreamsEventType + | CardsEventType + | TimesEventType + | SocialEventsEventType | BodyEventType | SystemEventType; @@ -422,6 +503,21 @@ export type ManaEvent = // Contacts | DomainEvent<'ContactCreated', ContactCreatedPayload> | DomainEvent<'ContactDeleted', ContactDeletedPayload> + // Finance + | DomainEvent<'TransactionCreated', TransactionCreatedPayload> + | DomainEvent<'TransactionDeleted', TransactionDeletedPayload> + // Dreams + | DomainEvent<'DreamCreated', DreamCreatedPayload> + | DomainEvent<'DreamDeleted', DreamDeletedPayload> + // Cards + | DomainEvent<'CardStudied', CardStudiedPayload> + | DomainEvent<'CardCreated', CardCreatedPayload> + // Times + | DomainEvent<'TimerStarted', TimerStartedPayload> + | DomainEvent<'TimerStopped', TimerStoppedPayload> + // Social Events + | DomainEvent<'SocialEventCreated', SocialEventCreatedPayload> + | DomainEvent<'SocialEventDeleted', SocialEventDeletedPayload> // Body | DomainEvent<'WorkoutStarted', WorkoutStartedPayload> | DomainEvent<'WorkoutFinished', WorkoutFinishedPayload> diff --git a/apps/mana/apps/web/src/lib/data/tools/init.ts b/apps/mana/apps/web/src/lib/data/tools/init.ts index 39cabaffa..90d97ca2e 100644 --- a/apps/mana/apps/web/src/lib/data/tools/init.ts +++ b/apps/mana/apps/web/src/lib/data/tools/init.ts @@ -14,6 +14,11 @@ import { journalTools } from '$lib/modules/journal/tools'; import { notesTools } from '$lib/modules/notes/tools'; import { contactsTools } from '$lib/modules/contacts/tools'; import { bodyTools } from '$lib/modules/body/tools'; +import { financeTools } from '$lib/modules/finance/tools'; +import { dreamsTools } from '$lib/modules/dreams/tools'; +import { cardsTools } from '$lib/modules/cards/tools'; +import { timesTools } from '$lib/modules/times/tools'; +import { socialEventsTools } from '$lib/modules/events/tools'; let initialized = false; @@ -29,5 +34,10 @@ export function initTools(): void { registerTools(notesTools); registerTools(contactsTools); registerTools(bodyTools); + registerTools(financeTools); + registerTools(dreamsTools); + registerTools(cardsTools); + registerTools(timesTools); + registerTools(socialEventsTools); initialized = true; } diff --git a/apps/mana/apps/web/src/lib/modules/cards/stores/cards.svelte.ts b/apps/mana/apps/web/src/lib/modules/cards/stores/cards.svelte.ts index fde0b03c8..586c255e5 100644 --- a/apps/mana/apps/web/src/lib/modules/cards/stores/cards.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/cards/stores/cards.svelte.ts @@ -9,6 +9,7 @@ import { CardsEvents } from '@mana/shared-utils/analytics'; import { cardTable, cardDeckTable } from '../collections'; import { toCard } from '../queries'; import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import type { LocalCard, Card, CreateCardInput, UpdateCardInput } from '../types'; let error = $state(null); @@ -44,6 +45,10 @@ export const cardStore = { }); } + emitDomainEvent('CardCreated', 'cards', 'cards', newLocal.id, { + cardId: newLocal.id, + deckId: input.deckId, + }); CardsEvents.cardCreated(); return plaintextSnapshot; } catch (err: any) { diff --git a/apps/mana/apps/web/src/lib/modules/cards/tools.ts b/apps/mana/apps/web/src/lib/modules/cards/tools.ts new file mode 100644 index 000000000..8133eb6da --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/cards/tools.ts @@ -0,0 +1,25 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +import { cardStore } from './stores/cards.svelte'; + +export const cardsTools: ModuleTool[] = [ + { + name: 'create_card', + module: 'cards', + description: 'Erstellt eine neue Lernkarte (Flashcard)', + parameters: [ + { name: 'deckId', type: 'string', description: 'ID des Decks', required: true }, + { name: 'front', type: 'string', description: 'Vorderseite (Frage)', required: true }, + { name: 'back', type: 'string', description: 'Rueckseite (Antwort)', required: true }, + ], + async execute(params) { + const card = await cardStore.createCard({ + deckId: params.deckId as string, + front: params.front as string, + back: params.back as string, + }); + return card + ? { success: true, data: card, message: 'Lernkarte erstellt' } + : { success: false, message: 'Fehler beim Erstellen der Karte' }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts b/apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts index e67c7605a..943f9ad8a 100644 --- a/apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts @@ -11,6 +11,7 @@ import { dreamSymbolTable, dreamTable } from '../collections'; import { toDream } from '../queries'; import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import { createBlock, deleteBlock } from '$lib/data/time-blocks/service'; import { transcribeAudio } from '$lib/voice/transcribe'; import type { @@ -118,6 +119,12 @@ export const dreamsStore = { await encryptRecord('dreams', newLocal); await dreamTable.add(newLocal); await this.touchSymbols(plaintextSnapshot.symbols, +1); + emitDomainEvent('DreamCreated', 'dreams', 'dreams', dreamId, { + dreamId, + title: data.title ?? undefined, + isLucid: data.isLucid ?? false, + mood: data.mood ?? undefined, + }); return plaintextSnapshot; }, @@ -304,6 +311,7 @@ export const dreamsStore = { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); + emitDomainEvent('DreamDeleted', 'dreams', 'dreams', id, { dreamId: id }); }, async togglePin(id: string) { diff --git a/apps/mana/apps/web/src/lib/modules/dreams/tools.ts b/apps/mana/apps/web/src/lib/modules/dreams/tools.ts new file mode 100644 index 000000000..e6a4b0c28 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/dreams/tools.ts @@ -0,0 +1,27 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +import { dreamsStore } from './stores/dreams.svelte'; + +export const dreamsTools: ModuleTool[] = [ + { + name: 'create_dream', + module: 'dreams', + description: 'Erstellt einen Traum-Eintrag im Traumtagebuch', + parameters: [ + { name: 'title', type: 'string', description: 'Titel des Traums', required: false }, + { name: 'content', type: 'string', description: 'Traumbeschreibung', required: true }, + { name: 'isLucid', type: 'boolean', description: 'Luzider Traum?', required: false }, + ], + async execute(params) { + const dream = await dreamsStore.createDream({ + title: params.title as string | undefined, + content: params.content as string, + isLucid: (params.isLucid as boolean) ?? false, + }); + return { + success: true, + data: dream, + message: `Traum "${dream.title || 'Unbenannt'}" erstellt`, + }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/events/stores/events.svelte.ts b/apps/mana/apps/web/src/lib/modules/events/stores/events.svelte.ts index 92d494a87..40797bbe3 100644 --- a/apps/mana/apps/web/src/lib/modules/events/stores/events.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/events/stores/events.svelte.ts @@ -9,6 +9,7 @@ import { db } from '$lib/data/database'; import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service'; import { timeBlockTable } from '$lib/data/time-blocks/collections'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import type { LocalSocialEvent, LocalEventItem, EventStatus } from '../types'; import { eventsApi } from '../api'; import { recordTombstone } from '../tombstones'; @@ -73,6 +74,11 @@ export const eventsStore = { // linked TimeBlock was already encrypted by createBlock above. await encryptRecord('socialEvents', newLocal); await db.table('socialEvents').add(newLocal); + emitDomainEvent('SocialEventCreated', 'events', 'socialEvents', eventId, { + eventId, + title: input.title, + date: input.startTime.split('T')[0], + }); return { success: true as const, id: eventId }; } catch (e) { error = e instanceof Error ? e.message : 'Failed to create event'; @@ -162,6 +168,7 @@ export const eventsStore = { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); + emitDomainEvent('SocialEventDeleted', 'events', 'socialEvents', id, { eventId: id }); return { success: true as const }; } catch (e) { error = e instanceof Error ? e.message : 'Failed to delete event'; diff --git a/apps/mana/apps/web/src/lib/modules/events/tools.ts b/apps/mana/apps/web/src/lib/modules/events/tools.ts new file mode 100644 index 000000000..ae7be50b1 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/events/tools.ts @@ -0,0 +1,29 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +import { eventsStore } from './stores/events.svelte'; + +export const socialEventsTools: ModuleTool[] = [ + { + name: 'create_social_event', + module: 'events', + description: 'Erstellt ein soziales Event (Party, Treffen, Feier)', + parameters: [ + { name: 'title', type: 'string', description: 'Name des Events', required: true }, + { name: 'startTime', type: 'string', description: 'Startzeit (ISO 8601)', required: true }, + { name: 'endTime', type: 'string', description: 'Endzeit (ISO 8601)', required: true }, + { name: 'location', type: 'string', description: 'Ort', required: false }, + { name: 'description', type: 'string', description: 'Beschreibung', required: false }, + ], + async execute(params) { + const result = await eventsStore.createEvent({ + title: params.title as string, + startTime: params.startTime as string, + endTime: params.endTime as string, + location: params.location as string | undefined, + description: params.description as string | undefined, + }); + return result.success + ? { success: true, data: { id: result.id }, message: `Event "${params.title}" erstellt` } + : { success: false, message: result.error ?? 'Fehler' }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts b/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts index ce7651eb8..6412a454d 100644 --- a/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts @@ -10,6 +10,7 @@ import { transactionTable, categoryTable } from '../collections'; import { toTransaction, toCategory } from '../queries'; import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import type { LocalTransaction, LocalFinanceCategory, TransactionType } from '../types'; export const financeStore = { @@ -34,6 +35,12 @@ export const financeStore = { const plaintextSnapshot = toTransaction(newLocal); await encryptRecord('transactions', newLocal); await transactionTable.add(newLocal); + emitDomainEvent('TransactionCreated', 'finance', 'transactions', newLocal.id, { + transactionId: newLocal.id, + amount: data.amount, + type: data.type, + description: data.description, + }); return plaintextSnapshot; }, @@ -56,6 +63,7 @@ export const financeStore = { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); + emitDomainEvent('TransactionDeleted', 'finance', 'transactions', id, { transactionId: id }); }, async addCategory(data: { name: string; emoji: string; color: string; type: TransactionType }) { diff --git a/apps/mana/apps/web/src/lib/modules/finance/tools.ts b/apps/mana/apps/web/src/lib/modules/finance/tools.ts new file mode 100644 index 000000000..c8d0344b1 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/finance/tools.ts @@ -0,0 +1,33 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +import { financeStore } from './stores/finance.svelte'; + +export const financeTools: ModuleTool[] = [ + { + name: 'add_transaction', + module: 'finance', + description: 'Erfasst eine Einnahme oder Ausgabe', + parameters: [ + { + name: 'type', + type: 'string', + description: 'Art', + required: true, + enum: ['income', 'expense'], + }, + { name: 'amount', type: 'number', description: 'Betrag in Euro', required: true }, + { name: 'description', type: 'string', description: 'Beschreibung', required: true }, + ], + async execute(params) { + const tx = await financeStore.addTransaction({ + type: params.type as 'income' | 'expense', + amount: params.amount as number, + description: params.description as string, + }); + return { + success: true, + data: tx, + message: `${params.type === 'income' ? 'Einnahme' : 'Ausgabe'}: ${params.amount}€ (${params.description})`, + }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/times/stores/timer.svelte.ts b/apps/mana/apps/web/src/lib/modules/times/stores/timer.svelte.ts index 73531274d..2afbed541 100644 --- a/apps/mana/apps/web/src/lib/modules/times/stores/timer.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/times/stores/timer.svelte.ts @@ -8,6 +8,7 @@ import { browser } from '$app/environment'; import { db } from '$lib/data/database'; +import { emitDomainEvent } from '$lib/data/events'; import { timeEntryTable, settingsTable } from '$lib/modules/times/collections'; import { roundDuration } from '$lib/modules/times/utils/rounding'; import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service'; @@ -135,6 +136,11 @@ export const timerStore = { elapsedSeconds = 0; startTicking(); startAutoSave(); + emitDomainEvent('TimerStarted', 'times', 'timeEntries', entryId, { + entryId, + description: options?.description, + projectId: options?.projectId, + }); }, /** Stop the running timer */ @@ -168,6 +174,11 @@ export const timerStore = { ...runningEntry, duration: roundedDuration, }; + emitDomainEvent('TimerStopped', 'times', 'timeEntries', runningEntry.id, { + entryId: runningEntry.id, + durationMinutes: Math.round(roundedDuration / 60), + description: runningEntry.description, + }); stopTicking(); runningEntry = null; runningBlock = null; diff --git a/apps/mana/apps/web/src/lib/modules/times/tools.ts b/apps/mana/apps/web/src/lib/modules/times/tools.ts new file mode 100644 index 000000000..789bbe031 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/times/tools.ts @@ -0,0 +1,41 @@ +import type { ModuleTool } from '$lib/data/tools/types'; + +export const timesTools: ModuleTool[] = [ + { + name: 'start_timer', + module: 'times', + description: 'Startet einen Zeitmess-Timer', + parameters: [ + { + name: 'description', + type: 'string', + description: 'Beschreibung der Taetigkeit', + required: false, + }, + ], + async execute(params) { + const { timerStore } = await import('./stores/timer.svelte'); + await timerStore.start({ description: params.description as string | undefined }); + return { + success: true, + message: `Timer gestartet${params.description ? `: "${params.description}"` : ''}`, + }; + }, + }, + { + name: 'stop_timer', + module: 'times', + description: 'Stoppt den laufenden Timer', + parameters: [], + async execute() { + const { timerStore } = await import('./stores/timer.svelte'); + const entry = await timerStore.stop(); + if (!entry) return { success: false, message: 'Kein Timer aktiv' }; + return { + success: true, + data: entry, + message: `Timer gestoppt (${Math.round(entry.duration / 60)} min)`, + }; + }, + }, +];