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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-13 23:00:01 +02:00
parent c51382a76e
commit 7752ba9ff9
12 changed files with 300 additions and 0 deletions

View file

@ -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>

View file

@ -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;
}

View file

@ -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<string | null>(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) {

View file

@ -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' };
},
},
];

View file

@ -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) {

View file

@ -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`,
};
},
},
];

View file

@ -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<LocalSocialEvent>('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';

View file

@ -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' };
},
},
];

View file

@ -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 }) {

View file

@ -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})`,
};
},
},
];

View file

@ -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;

View file

@ -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)`,
};
},
},
];