mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
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:
parent
c51382a76e
commit
7752ba9ff9
12 changed files with 300 additions and 0 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
25
apps/mana/apps/web/src/lib/modules/cards/tools.ts
Normal file
25
apps/mana/apps/web/src/lib/modules/cards/tools.ts
Normal 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' };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
27
apps/mana/apps/web/src/lib/modules/dreams/tools.ts
Normal file
27
apps/mana/apps/web/src/lib/modules/dreams/tools.ts
Normal 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`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
29
apps/mana/apps/web/src/lib/modules/events/tools.ts
Normal file
29
apps/mana/apps/web/src/lib/modules/events/tools.ts
Normal 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' };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -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 }) {
|
||||
|
|
|
|||
33
apps/mana/apps/web/src/lib/modules/finance/tools.ts
Normal file
33
apps/mana/apps/web/src/lib/modules/finance/tools.ts
Normal 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})`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
41
apps/mana/apps/web/src/lib/modules/times/tools.ts
Normal file
41
apps/mana/apps/web/src/lib/modules/times/tools.ts
Normal 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)`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue