feat(ai): add AI tools for myday, goals, mood, finance, and times

Expand agent tool coverage from 28 to 47 tools across 16 modules:

- myday: get_myday_summary (full daily context in one call)
- goals: list_goals, get_goal_progress, create_goal, pause/resume/complete_goal
- mood: log_mood, get_mood_today, get_mood_insights (trends + correlations)
- finance: extend add_transaction, add get_month_summary + list_transactions
- times: extend start/stop_timer, add get_timer_status, get_time_stats, list_projects

All tools registered in both AI_TOOL_CATALOG (shared-ai) and webapp init.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-16 15:01:12 +02:00
parent acd7e0d6b0
commit ed01d24f2d
7 changed files with 1299 additions and 6 deletions

View file

@ -35,6 +35,9 @@ import { recipesTools } from '$lib/modules/recipes/tools';
import { questionsTools } from '$lib/modules/questions/tools';
import { meditateTools } from '$lib/modules/meditate/tools';
import { sleepTools } from '$lib/modules/sleep/tools';
import { mydayTools } from '$lib/modules/myday/tools';
import { goalsTools } from '$lib/modules/goals/tools';
import { moodTools } from '$lib/modules/mood/tools';
let initialized = false;
@ -71,5 +74,8 @@ export function initTools(): void {
registerTools(questionsTools);
registerTools(meditateTools);
registerTools(sleepTools);
registerTools(mydayTools);
registerTools(goalsTools);
registerTools(moodTools);
initialized = true;
}

View file

@ -1,5 +1,13 @@
/**
* Finance Tools LLM-accessible operations for income/expense tracking.
*/
import type { ModuleTool } from '$lib/data/tools/types';
import { financeStore } from './stores/finance.svelte';
import { transactionTable, categoryTable } from './collections';
import { decryptRecords } from '$lib/data/crypto';
import { toTransaction, toCategory, currentMonth, formatCurrency } from './queries';
import type { LocalTransaction, LocalFinanceCategory, TransactionType } from './types';
export const financeTools: ModuleTool[] = [
{
@ -16,17 +24,143 @@ export const financeTools: ModuleTool[] = [
},
{ name: 'amount', type: 'number', description: 'Betrag in Euro', required: true },
{ name: 'description', type: 'string', description: 'Beschreibung', required: true },
{
name: 'date',
type: 'string',
description: 'Datum (YYYY-MM-DD, Standard: heute)',
required: false,
},
],
async execute(params) {
const tx = await financeStore.addTransaction({
type: params.type as 'income' | 'expense',
type: params.type as TransactionType,
amount: params.amount as number,
description: params.description as string,
date: params.date as string | undefined,
});
return {
success: true,
data: tx,
message: `${params.type === 'income' ? 'Einnahme' : 'Ausgabe'}: ${params.amount}€ (${params.description})`,
message: `${params.type === 'income' ? 'Einnahme' : 'Ausgabe'}: ${formatCurrency(params.amount as number)} (${params.description})`,
};
},
},
{
name: 'get_month_summary',
module: 'finance',
description:
'Gibt die Finanz-Zusammenfassung fuer einen Monat zurueck: Einnahmen, Ausgaben, Bilanz, Ausgaben pro Kategorie.',
parameters: [
{
name: 'month',
type: 'string',
description: 'Monat im Format YYYY-MM (Standard: aktueller Monat)',
required: false,
},
],
async execute(params) {
const month = (params.month as string) ?? currentMonth();
const [allTxs, allCats] = await Promise.all([
transactionTable.toArray(),
categoryTable.toArray(),
]);
const visible = allTxs.filter((t) => !t.deletedAt);
const decrypted = await decryptRecords<LocalTransaction>('transactions', visible);
const txs = decrypted.map(toTransaction);
const cats = allCats.filter((c) => !c.deletedAt).map(toCategory);
const monthTxs = txs.filter((t) => t.date.startsWith(month));
let income = 0;
let expenses = 0;
const byCategory = new Map<string, number>();
for (const tx of monthTxs) {
if (tx.type === 'income') income += tx.amount;
else {
expenses += tx.amount;
if (tx.categoryId) {
byCategory.set(tx.categoryId, (byCategory.get(tx.categoryId) ?? 0) + tx.amount);
}
}
}
const catBreakdown = [...byCategory.entries()]
.sort((a, b) => b[1] - a[1])
.map(([catId, amount]) => {
const cat = cats.find((c) => c.id === catId);
return { category: cat ? `${cat.emoji} ${cat.name}` : 'Sonstiges', amount };
});
return {
success: true,
data: {
month,
income,
expenses,
balance: income - expenses,
transactions: monthTxs.length,
byCategory: catBreakdown,
},
message: `${month}: ${formatCurrency(income)} Einnahmen, ${formatCurrency(expenses)} Ausgaben, Bilanz: ${formatCurrency(income - expenses)}`,
};
},
},
{
name: 'list_transactions',
module: 'finance',
description:
'Listet die letzten Transaktionen auf. Optional nach Typ (income/expense) und Monat filterbar.',
parameters: [
{
name: 'type',
type: 'string',
description: 'Nur income oder expense zeigen',
required: false,
enum: ['income', 'expense'],
},
{
name: 'month',
type: 'string',
description: 'Monat im Format YYYY-MM',
required: false,
},
{
name: 'limit',
type: 'number',
description: 'Maximale Anzahl (Standard: 20)',
required: false,
},
],
async execute(params) {
const filterType = params.type as TransactionType | undefined;
const month = params.month as string | undefined;
const limit = (params.limit as number) ?? 20;
const all = await transactionTable.toArray();
const visible = all.filter((t) => !t.deletedAt);
const decrypted = await decryptRecords<LocalTransaction>('transactions', visible);
let txs = decrypted.map(toTransaction);
if (filterType) txs = txs.filter((t) => t.type === filterType);
if (month) txs = txs.filter((t) => t.date.startsWith(month));
txs.sort((a, b) => b.date.localeCompare(a.date) || b.createdAt.localeCompare(a.createdAt));
const sliced = txs.slice(0, limit);
return {
success: true,
data: sliced.map((t) => ({
id: t.id,
type: t.type,
amount: t.amount,
description: t.description,
date: t.date,
})),
message: `${sliced.length} Transaktionen${filterType ? ` (${filterType})` : ''}${month ? ` in ${month}` : ''}`,
};
},
},

View file

@ -0,0 +1,253 @@
/**
* Goals Tools LLM-accessible operations for the goal system.
*
* Goals are event-driven: progress auto-increments when matching
* domain events fire (e.g. DrinkLogged, TaskCompleted). These tools
* let agents read progress, create new goals, and manage status.
*/
import type { ModuleTool } from '$lib/data/tools/types';
import { db } from '$lib/data/database';
import { goalStore } from '$lib/companion/goals/store';
import { GOAL_TEMPLATES, type LocalGoal } from '$lib/companion/goals/types';
const TABLE = 'companionGoals';
export const goalsTools: ModuleTool[] = [
{
name: 'list_goals',
module: 'goals',
description:
'Listet alle Ziele mit aktuellem Fortschritt auf. Zeigt Titel, Fortschritt, Zielwert, Zeitraum und Status.',
parameters: [
{
name: 'filter',
type: 'string',
description: 'Welche Ziele zeigen',
required: false,
enum: ['active', 'paused', 'completed', 'all'],
},
],
async execute(params) {
const filter = (params.filter as string) ?? 'active';
const all = await db.table<LocalGoal>(TABLE).toArray();
const visible = all.filter((g) => !g.deletedAt);
const filtered = filter === 'all' ? visible : visible.filter((g) => g.status === filter);
const items = filtered.map((g) => ({
id: g.id,
title: g.title,
description: g.description,
moduleId: g.moduleId,
status: g.status,
current: g.currentValue,
target: g.target.value,
period: g.target.period,
comparison: g.target.comparison,
percent:
g.target.comparison === 'gte'
? Math.min(Math.round((g.currentValue / g.target.value) * 100), 100)
: g.currentValue <= g.target.value
? 100
: Math.max(
0,
Math.round((1 - (g.currentValue - g.target.value) / g.target.value) * 100)
),
}));
return {
success: true,
data: items,
message: `${items.length} Ziele (${filter}): ${items.map((g) => `${g.title} ${g.current}/${g.target}`).join(', ')}`,
};
},
},
{
name: 'get_goal_progress',
module: 'goals',
description:
'Gibt den detaillierten Fortschritt eines einzelnen Ziels zurueck, inklusive Metrik-Details und Periodeninfo.',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
async execute(params) {
const goalId = params.goalId as string;
const goal = await db.table<LocalGoal>(TABLE).get(goalId);
if (!goal || goal.deletedAt) {
return { success: false, message: `Ziel ${goalId} nicht gefunden` };
}
const reached =
goal.target.comparison === 'gte'
? goal.currentValue >= goal.target.value
: goal.currentValue <= goal.target.value;
return {
success: true,
data: {
id: goal.id,
title: goal.title,
description: goal.description,
moduleId: goal.moduleId,
status: goal.status,
current: goal.currentValue,
target: goal.target.value,
period: goal.target.period,
comparison: goal.target.comparison,
periodStart: goal.currentPeriodStart,
reached,
metric: goal.metric,
},
message: `${goal.title}: ${goal.currentValue}/${goal.target.value} (${goal.target.period}) — ${reached ? 'erreicht' : 'offen'}`,
};
},
},
{
name: 'create_goal',
module: 'goals',
description:
'Erstellt ein neues Ziel. Kann entweder ein Template verwenden (templateId) oder ein benutzerdefiniertes Ziel erstellen. Verfuegbare Templates: tpl-water-daily, tpl-tasks-daily, tpl-meals-daily, tpl-calories-daily, tpl-places-weekly, tpl-coffee-limit.',
parameters: [
{
name: 'templateId',
type: 'string',
description:
'ID eines Templates (z.B. "tpl-water-daily"). Wenn gesetzt, werden andere Felder ignoriert.',
required: false,
},
{
name: 'title',
type: 'string',
description: 'Titel des Ziels (nur fuer benutzerdefinierte Ziele)',
required: false,
},
{
name: 'description',
type: 'string',
description: 'Beschreibung',
required: false,
},
{
name: 'targetValue',
type: 'number',
description: 'Zielwert (z.B. 8 fuer "8 Glaeser Wasser")',
required: false,
},
{
name: 'period',
type: 'string',
description: 'Zeitraum',
required: false,
enum: ['day', 'week', 'month'],
},
{
name: 'comparison',
type: 'string',
description: 'Vergleich: gte = mindestens, lte = hoechstens',
required: false,
enum: ['gte', 'lte'],
},
{
name: 'eventType',
type: 'string',
description:
'Domain-Event zum Zaehlen (z.B. "DrinkLogged", "TaskCompleted", "MealLogged", "WorkoutFinished")',
required: false,
},
{
name: 'moduleId',
type: 'string',
description: 'Zugehoeriges Modul (z.B. "drink", "todo", "food", "body")',
required: false,
},
],
async execute(params) {
// Template-based creation
const templateId = params.templateId as string | undefined;
if (templateId) {
const template = GOAL_TEMPLATES.find((t) => t.id === templateId);
if (!template) {
return { success: false, message: `Template "${templateId}" nicht gefunden` };
}
const goal = await goalStore.createFromTemplate(template);
return {
success: true,
data: { id: goal.id, title: goal.title },
message: `Ziel "${goal.title}" aus Template erstellt`,
};
}
// Custom goal creation
const title = params.title as string | undefined;
if (!title) {
return { success: false, message: 'Entweder templateId oder title ist erforderlich' };
}
const eventType = params.eventType as string | undefined;
if (!eventType) {
return {
success: false,
message: 'eventType ist fuer benutzerdefinierte Ziele erforderlich',
};
}
const goal = await goalStore.create({
title,
description: params.description as string | undefined,
moduleId: (params.moduleId as string) ?? 'general',
metric: {
source: 'event_count',
eventType,
},
target: {
value: (params.targetValue as number) ?? 1,
period: (params.period as 'day' | 'week' | 'month') ?? 'day',
comparison: (params.comparison as 'gte' | 'lte') ?? 'gte',
},
});
return {
success: true,
data: { id: goal.id, title: goal.title },
message: `Ziel "${goal.title}" erstellt`,
};
},
},
{
name: 'pause_goal',
module: 'goals',
description: 'Pausiert ein aktives Ziel. Kann spaeter wieder fortgesetzt werden.',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
async execute(params) {
const goalId = params.goalId as string;
await goalStore.pause(goalId);
return { success: true, message: `Ziel ${goalId} pausiert` };
},
},
{
name: 'resume_goal',
module: 'goals',
description: 'Setzt ein pausiertes Ziel fort.',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
async execute(params) {
const goalId = params.goalId as string;
await goalStore.resume(goalId);
return { success: true, message: `Ziel ${goalId} fortgesetzt` };
},
},
{
name: 'complete_goal',
module: 'goals',
description: 'Markiert ein Ziel als abgeschlossen.',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
async execute(params) {
const goalId = params.goalId as string;
await goalStore.complete(goalId);
return { success: true, message: `Ziel ${goalId} abgeschlossen` };
},
},
];

View file

@ -0,0 +1,199 @@
/**
* Mood Tools LLM-accessible operations for mood tracking.
*/
import type { ModuleTool } from '$lib/data/tools/types';
import { moodStore } from './stores/mood.svelte';
import { moodEntryTable } from './collections';
import { decryptRecords } from '$lib/data/crypto';
import {
getAvgLevel,
getTopEmotion,
getValenceRatio,
getActivityInsights,
toMoodEntry,
} from './queries';
import { EMOTION_META, type CoreEmotion, type ActivityContext, type LocalMoodEntry } from './types';
function todayStr(): string {
return new Date().toISOString().split('T')[0];
}
export const moodTools: ModuleTool[] = [
{
name: 'log_mood',
module: 'mood',
description:
'Erfasst einen Mood-Check-in mit Level (1-10), primaerer Emotion und optionalem Kontext.',
parameters: [
{
name: 'level',
type: 'number',
description: 'Stimmungs-Level von 1 (schlecht) bis 10 (super)',
required: true,
},
{
name: 'emotion',
type: 'string',
description: 'Primaere Emotion',
required: true,
enum: [
'happy',
'calm',
'energized',
'grateful',
'excited',
'loved',
'hopeful',
'neutral',
'bored',
'tired',
'sad',
'anxious',
'angry',
'stressed',
'frustrated',
'overwhelmed',
],
},
{
name: 'activity',
type: 'string',
description: 'Was machst du gerade?',
required: false,
enum: [
'work',
'exercise',
'social',
'alone',
'commute',
'eating',
'resting',
'creative',
'outdoors',
'screen',
'chores',
'other',
],
},
{
name: 'notes',
type: 'string',
description: 'Optionale Notiz zum Check-in',
required: false,
},
],
async execute(params) {
const entry = await moodStore.logMood({
level: params.level as number,
emotion: params.emotion as CoreEmotion,
activity: (params.activity as ActivityContext) ?? null,
notes: (params.notes as string) ?? '',
});
const meta = EMOTION_META[params.emotion as CoreEmotion];
return {
success: true,
data: entry,
message: `Mood geloggt: ${meta.emoji} ${meta.de} (Level ${params.level}/10)`,
};
},
},
{
name: 'get_mood_today',
module: 'mood',
description: 'Gibt alle heutigen Mood-Eintraege zurueck mit Durchschnitts-Level und Emotionen.',
parameters: [],
async execute() {
const today = todayStr();
const all = await moodEntryTable.toArray();
const todayEntries = all.filter((e) => !e.deletedAt && e.date === today);
const decrypted = await decryptRecords<LocalMoodEntry>('moodEntries', todayEntries);
const entries = decrypted.map(toMoodEntry);
if (entries.length === 0) {
return {
success: true,
data: { entries: [], avgLevel: 0 },
message: 'Noch kein Mood-Eintrag heute',
};
}
const avgLevel = +(entries.reduce((s, e) => s + e.level, 0) / entries.length).toFixed(1);
const emotions = entries.map((e) => {
const meta = EMOTION_META[e.emotion];
return {
emotion: e.emotion,
label: meta.de,
emoji: meta.emoji,
level: e.level,
time: e.time,
};
});
return {
success: true,
data: { entries: emotions, avgLevel, count: entries.length },
message: `${entries.length} Check-ins heute, Durchschnitt: ${avgLevel}/10`,
};
},
},
{
name: 'get_mood_insights',
module: 'mood',
description:
'Gibt Mood-Trends und Muster zurueck: Durchschnitt der letzten 7/30 Tage, haeufigste Emotion, Positiv/Negativ-Verhaeltnis, und welche Aktivitaeten mit guter/schlechter Stimmung korrelieren.',
parameters: [
{
name: 'days',
type: 'number',
description: 'Analyse-Zeitraum in Tagen (Standard: 7)',
required: false,
},
],
async execute(params) {
const days = (params.days as number) ?? 7;
const all = await moodEntryTable.toArray();
const visible = all.filter((e) => !e.deletedAt);
const decrypted = await decryptRecords<LocalMoodEntry>('moodEntries', visible);
const entries = decrypted.map(toMoodEntry);
// Filter to requested time window
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
const cutoffStr = cutoff.toISOString().split('T')[0];
const windowEntries = entries.filter((e) => e.date >= cutoffStr);
if (windowEntries.length === 0) {
return {
success: true,
data: null,
message: `Keine Mood-Daten in den letzten ${days} Tagen`,
};
}
const avgLevel = getAvgLevel(entries, days);
const topEmotion = getTopEmotion(windowEntries, windowEntries.length);
const valence = getValenceRatio(windowEntries);
const activities = getActivityInsights(windowEntries);
const topEmotionMeta = topEmotion ? EMOTION_META[topEmotion] : null;
return {
success: true,
data: {
period: `${days} Tage`,
totalEntries: windowEntries.length,
avgLevel,
topEmotion: topEmotion
? { emotion: topEmotion, label: topEmotionMeta!.de, emoji: topEmotionMeta!.emoji }
: null,
valence,
activityCorrelations: activities.slice(0, 5),
},
message: `${days}d: Ø ${avgLevel}/10, ${topEmotionMeta ? `meist ${topEmotionMeta.emoji} ${topEmotionMeta.de}` : ''}, ${valence.positive}% positiv / ${valence.negative}% negativ`,
};
},
},
];

View file

@ -0,0 +1,198 @@
/**
* MyDay Tools LLM-accessible day summary.
*
* Single read-only tool that aggregates today's snapshot + streaks
* into one response. Gives the agent full daily context in one call.
*/
import type { ModuleTool } from '$lib/data/tools/types';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
import { DEFAULT_DAILY_GOAL_ML } from '$lib/modules/drink/types';
import { DEFAULT_DAILY_VALUES } from '$lib/modules/food/constants';
import type { LocalTask } from '$lib/modules/todo/types';
import type { LocalDrinkEntry } from '$lib/modules/drink/types';
import type { LocalMeal, LocalGoal as NutriGoal } from '$lib/modules/food/types';
import type { LocalPlace } from '$lib/modules/places/types';
import type { LocalTimeBlock } from '$lib/data/time-blocks/types';
import type { LocalGoal } from '$lib/companion/goals/types';
function todayStr(): string {
return new Date().toISOString().split('T')[0];
}
export const mydayTools: ModuleTool[] = [
{
name: 'get_myday_summary',
module: 'myday',
description:
'Gibt eine komplette Tageszusammenfassung zurueck: Tasks, Termine, Trinken, Ernaehrung, Orte, Habits/Streaks und aktive Ziele. Nutze dieses Tool zuerst, um den vollen Tageskontext zu bekommen.',
parameters: [],
async execute() {
const today = todayStr();
const now = new Date().toISOString();
const todayStart = `${today}T00:00:00`;
const todayEnd = `${today}T23:59:59`;
// ── Parallel queries ────────────────────────
const [allTasks, blocks, allDrinks, allMeals, foodGoals, allPlaces, streakStates, goals] =
await Promise.all([
db.table<LocalTask>('tasks').toArray(),
db
.table<LocalTimeBlock>('timeBlocks')
.where('startDate')
.between(todayStart, todayEnd + '\uffff')
.toArray(),
db.table<LocalDrinkEntry>('drinkEntries').toArray(),
db.table<LocalMeal>('meals').toArray(),
db.table<NutriGoal>('goals').toArray(),
db.table<LocalPlace>('places').toArray(),
db.table('_streakState').toArray(),
db.table<LocalGoal>('companionGoals').toArray(),
]);
// ── Filter + decrypt ────────────────────────
const activeTasks = allTasks.filter((t) => !t.deletedAt);
const eventBlocks = blocks.filter(
(b) => !b.deletedAt && b.type === 'event' && b.sourceModule === 'calendar'
);
const todayDrinks = allDrinks.filter((d) => !d.deletedAt && d.date === today);
const todayMeals = allMeals.filter((m) => !m.deletedAt && m.date === today);
const [decTasks, decBlocks, decDrinks, decMeals] = await Promise.all([
decryptRecords<LocalTask>('tasks', activeTasks),
decryptRecords<LocalTimeBlock>('timeBlocks', eventBlocks),
decryptRecords<LocalDrinkEntry>('drinkEntries', todayDrinks),
decryptRecords<LocalMeal>('meals', todayMeals),
]);
// ── Tasks ───────────────────────────────────
const openTasks = decTasks.filter((t) => !t.isCompleted);
const completedCount = decTasks.filter((t) => t.isCompleted).length;
const overdue = openTasks.filter((t) => t.dueDate != null && (t.dueDate as string) < today);
const dueToday = openTasks.filter((t) => (t.dueDate as string) === today);
// ── Events ──────────────────────────────────
const events = decBlocks
.sort((a, b) => (a.startDate as string).localeCompare(b.startDate as string))
.map((b) => ({
title: (b.title as string) ?? '',
startTime: b.startDate,
endTime: b.endDate ?? b.startDate,
isAllDay: b.allDay ?? false,
}));
const upcoming = events.filter((e) => e.startTime >= now);
// ── Drinks ──────────────────────────────────
let waterMl = 0;
let coffeeMl = 0;
let coffeeCount = 0;
let totalMl = 0;
for (const d of decDrinks) {
const ml = d.quantityMl ?? 0;
totalMl += ml;
if (d.drinkType === 'water') waterMl += ml;
if (d.drinkType === 'coffee') {
coffeeMl += ml;
coffeeCount++;
}
}
// ── Nutrition ───────────────────────────────
let totalCalories = 0;
let totalProtein = 0;
for (const m of decMeals) {
const n = m.nutrition as { calories?: number; protein?: number } | null;
if (n) {
totalCalories += n.calories ?? 0;
totalProtein += n.protein ?? 0;
}
}
const activeGoal = foodGoals.find((g) => !g.deletedAt);
const calorieGoal = activeGoal?.dailyCalories ?? DEFAULT_DAILY_VALUES.calories;
// ── Places ──────────────────────────────────
const visitedToday = allPlaces.filter(
(p) => !p.deletedAt && p.lastVisitedAt && (p.lastVisitedAt as string).startsWith(today)
).length;
// ── Streaks ─────────────────────────────────
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayS = yesterday.toISOString().split('T')[0];
const streaks = (streakStates as Array<Record<string, unknown>>)
.filter((s) => s.lastActiveDate === today || s.lastActiveDate === yesterdayS)
.map((s) => ({
label: s.label as string,
streak: s.currentStreak as number,
status: s.lastActiveDate === today ? 'active' : 'at_risk',
}));
// ── Active Goals ────────────────────────────
const activeGoals = goals
.filter((g) => g.status === 'active' && !g.deletedAt)
.map((g) => ({
title: g.title,
current: g.currentValue,
target: g.target.value,
period: g.target.period,
percent: Math.round((g.currentValue / g.target.value) * 100),
}));
const summary = {
date: today,
tasks: {
open: openTasks.length,
completed: completedCount,
overdue: overdue.length,
dueToday: dueToday.slice(0, 10).map((t) => ({
title: (t.title as string) ?? '',
priority: t.priority as string | undefined,
})),
},
events: {
total: events.length,
upcoming: upcoming.slice(0, 5).map((e) => ({
title: e.title,
startTime: e.startTime,
isAllDay: e.isAllDay,
})),
},
drinks: {
water: {
ml: waterMl,
goal: DEFAULT_DAILY_GOAL_ML,
percent: Math.round((waterMl / DEFAULT_DAILY_GOAL_ML) * 100),
},
coffee: { count: coffeeCount },
total: { ml: totalMl, count: decDrinks.length },
},
nutrition: {
meals: decMeals.length,
calories: { actual: Math.round(totalCalories), goal: calorieGoal },
},
places: { visitedToday },
streaks,
goals: activeGoals,
};
const parts: string[] = [];
parts.push(
`${today}: ${openTasks.length} offene Tasks (${completedCount} erledigt, ${overdue.length} ueberfaellig)`
);
if (upcoming.length > 0)
parts.push(`${events.length} Termine (naechster: ${upcoming[0].title})`);
parts.push(`Wasser: ${waterMl}/${DEFAULT_DAILY_GOAL_ML}ml, ${coffeeCount} Kaffee`);
if (decMeals.length > 0)
parts.push(`${decMeals.length} Mahlzeiten, ${Math.round(totalCalories)} kcal`);
if (activeGoals.length > 0) parts.push(`${activeGoals.length} aktive Ziele`);
return {
success: true,
data: summary,
message: parts.join(' · '),
};
},
},
];

View file

@ -1,10 +1,18 @@
/**
* Times Tools LLM-accessible operations for time tracking.
*/
import type { ModuleTool } from '$lib/data/tools/types';
import { db } from '$lib/data/database';
import { formatDurationCompact, toTimeEntry, toProject, toClient } from './queries';
import type { LocalTimeEntry, LocalProject, LocalClient } from './types';
import type { LocalTimeBlock } from '$lib/data/time-blocks/types';
export const timesTools: ModuleTool[] = [
{
name: 'start_timer',
module: 'times',
description: 'Startet einen Zeitmess-Timer',
description: 'Startet einen Zeitmess-Timer mit optionaler Beschreibung und Projekt.',
parameters: [
{
name: 'description',
@ -12,20 +20,30 @@ export const timesTools: ModuleTool[] = [
description: 'Beschreibung der Taetigkeit',
required: false,
},
{
name: 'projectId',
type: 'string',
description: 'ID eines Projekts (aus list_projects)',
required: false,
},
],
async execute(params) {
const { timerStore } = await import('./stores/timer.svelte');
await timerStore.start({ description: params.description as string | undefined });
await timerStore.start({
description: params.description as string | undefined,
projectId: params.projectId as string | undefined,
});
return {
success: true,
message: `Timer gestartet${params.description ? `: "${params.description}"` : ''}`,
};
},
},
{
name: 'stop_timer',
module: 'times',
description: 'Stoppt den laufenden Timer',
description: 'Stoppt den laufenden Timer und speichert den Zeiteintrag.',
parameters: [],
async execute() {
const { timerStore } = await import('./stores/timer.svelte');
@ -34,7 +52,158 @@ export const timesTools: ModuleTool[] = [
return {
success: true,
data: entry,
message: `Timer gestoppt (${Math.round(entry.duration / 60)} min)`,
message: `Timer gestoppt (${formatDurationCompact(entry.duration)})`,
};
},
},
{
name: 'get_timer_status',
module: 'times',
description: 'Gibt den Status des laufenden Timers zurueck (ob aktiv, Dauer, Beschreibung).',
parameters: [],
async execute() {
const { timerStore } = await import('./stores/timer.svelte');
if (!timerStore.isRunning) {
return { success: true, data: { running: false }, message: 'Kein Timer aktiv' };
}
const entry = timerStore.runningEntry;
return {
success: true,
data: {
running: true,
elapsed: timerStore.elapsedSeconds,
elapsedFormatted: formatDurationCompact(timerStore.elapsedSeconds),
description: entry?.description ?? '',
projectId: entry?.projectId ?? null,
},
message: `Timer laeuft: ${formatDurationCompact(timerStore.elapsedSeconds)}${entry?.description ? ` — "${entry.description}"` : ''}`,
};
},
},
{
name: 'get_time_stats',
module: 'times',
description:
'Gibt Zeiterfassungs-Statistiken zurueck: Stunden heute, diese Woche, und Aufschluesselung nach Projekt.',
parameters: [
{
name: 'period',
type: 'string',
description: 'Zeitraum (Standard: week)',
required: false,
enum: ['today', 'week', 'month'],
},
],
async execute(params) {
const period = (params.period as string) ?? 'week';
const now = new Date();
const todayStr = now.toISOString().split('T')[0];
// Determine date range
let fromDate: string;
if (period === 'today') {
fromDate = todayStr;
} else if (period === 'week') {
const d = new Date(now);
d.setDate(d.getDate() - d.getDay() + (d.getDay() === 0 ? -6 : 1)); // Monday
fromDate = d.toISOString().split('T')[0];
} else {
fromDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
}
// Fetch entries + blocks + projects
const [allEntries, allBlocks, allProjects, allClients] = await Promise.all([
db.table<LocalTimeEntry>('timeEntries').toArray(),
db.table<LocalTimeBlock>('timeBlocks').toArray(),
db.table<LocalProject>('timeProjects').toArray(),
db.table<LocalClient>('timeClients').toArray(),
]);
const blocksById = new Map(allBlocks.map((b) => [b.id, b]));
const entries = allEntries
.filter((e) => !e.deletedAt)
.map((e) => toTimeEntry(e, blocksById.get(e.timeBlockId)))
.filter((e) => e.date >= fromDate && e.date <= todayStr);
const projects = allProjects.filter((p) => !p.deletedAt).map(toProject);
const clients = allClients.filter((c) => !c.deletedAt).map(toClient);
// Aggregate
const totalSeconds = entries.reduce((s, e) => s + e.duration, 0);
const billableSeconds = entries
.filter((e) => e.isBillable)
.reduce((s, e) => s + e.duration, 0);
// By project
const byProject = new Map<string, number>();
for (const e of entries) {
const key = e.projectId ?? '_none';
byProject.set(key, (byProject.get(key) ?? 0) + e.duration);
}
const projectBreakdown = [...byProject.entries()]
.sort((a, b) => b[1] - a[1])
.map(([projId, secs]) => {
const proj = projects.find((p) => p.id === projId);
const client = proj?.clientId ? clients.find((c) => c.id === proj.clientId) : null;
return {
project: proj?.name ?? 'Ohne Projekt',
client: client?.name ?? null,
duration: formatDurationCompact(secs),
seconds: secs,
};
});
return {
success: true,
data: {
period,
from: fromDate,
to: todayStr,
entries: entries.length,
total: formatDurationCompact(totalSeconds),
totalSeconds,
billable: formatDurationCompact(billableSeconds),
billableSeconds,
byProject: projectBreakdown,
},
message: `${period}: ${formatDurationCompact(totalSeconds)} gesamt (${formatDurationCompact(billableSeconds)} abrechenbar), ${entries.length} Eintraege`,
};
},
},
{
name: 'list_projects',
module: 'times',
description: 'Listet alle aktiven Zeiterfassungs-Projekte mit Kunden-Info auf.',
parameters: [],
async execute() {
const [allProjects, allClients] = await Promise.all([
db.table<LocalProject>('timeProjects').toArray(),
db.table<LocalClient>('timeClients').toArray(),
]);
const clients = allClients.filter((c) => !c.deletedAt).map(toClient);
const projects = allProjects
.filter((p) => !p.deletedAt && !p.isArchived)
.map(toProject)
.map((p) => {
const client = p.clientId ? clients.find((c) => c.id === p.clientId) : null;
return {
id: p.id,
name: p.name,
client: client?.name ?? null,
isBillable: p.isBillable,
color: p.color,
};
});
return {
success: true,
data: projects,
message: `${projects.length} aktive Projekte`,
};
},
},

View file

@ -488,6 +488,124 @@ export const AI_TOOL_CATALOG: readonly ToolSchema[] = [
parameters: [],
},
// ── MyDay ─────────────────────────────────────────────────
{
name: 'get_myday_summary',
module: 'myday',
description:
'Gibt eine komplette Tageszusammenfassung zurueck: Tasks, Termine, Trinken, Ernaehrung, Orte, Streaks und aktive Ziele. Nutze dieses Tool zuerst, um den vollen Tageskontext zu bekommen.',
defaultPolicy: 'auto',
parameters: [],
},
// ── Goals ─────────────────────────────────────────────────
{
name: 'list_goals',
module: 'goals',
description:
'Listet alle Ziele mit aktuellem Fortschritt auf. Zeigt Titel, Fortschritt, Zielwert, Zeitraum und Status.',
defaultPolicy: 'auto',
parameters: [
{
name: 'filter',
type: 'string',
description: 'Welche Ziele zeigen',
required: false,
enum: ['active', 'paused', 'completed', 'all'],
},
],
},
{
name: 'get_goal_progress',
module: 'goals',
description:
'Gibt den detaillierten Fortschritt eines einzelnen Ziels zurueck, inklusive Metrik-Details und Periodeninfo.',
defaultPolicy: 'auto',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
},
{
name: 'create_goal',
module: 'goals',
description:
'Erstellt ein neues Ziel. Kann entweder ein Template verwenden (templateId) oder ein benutzerdefiniertes Ziel erstellen. Verfuegbare Templates: tpl-water-daily, tpl-tasks-daily, tpl-meals-daily, tpl-calories-daily, tpl-places-weekly, tpl-coffee-limit.',
defaultPolicy: 'propose',
parameters: [
{
name: 'templateId',
type: 'string',
description:
'ID eines Templates (z.B. "tpl-water-daily"). Wenn gesetzt, werden andere Felder ignoriert.',
required: false,
},
{
name: 'title',
type: 'string',
description: 'Titel des Ziels (nur fuer benutzerdefinierte Ziele)',
required: false,
},
{
name: 'description',
type: 'string',
description: 'Beschreibung',
required: false,
},
{
name: 'targetValue',
type: 'number',
description: 'Zielwert (z.B. 8 fuer "8 Glaeser Wasser")',
required: false,
},
{
name: 'period',
type: 'string',
description: 'Zeitraum',
required: false,
enum: ['day', 'week', 'month'],
},
{
name: 'comparison',
type: 'string',
description: 'Vergleich: gte = mindestens, lte = hoechstens',
required: false,
enum: ['gte', 'lte'],
},
{
name: 'eventType',
type: 'string',
description:
'Domain-Event zum Zaehlen (z.B. "DrinkLogged", "TaskCompleted", "MealLogged", "WorkoutFinished")',
required: false,
},
{
name: 'moduleId',
type: 'string',
description: 'Zugehoeriges Modul (z.B. "drink", "todo", "food", "body")',
required: false,
},
],
},
{
name: 'pause_goal',
module: 'goals',
description: 'Pausiert ein aktives Ziel. Kann spaeter wieder fortgesetzt werden.',
defaultPolicy: 'propose',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
},
{
name: 'resume_goal',
module: 'goals',
description: 'Setzt ein pausiertes Ziel fort.',
defaultPolicy: 'propose',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
},
{
name: 'complete_goal',
module: 'goals',
description: 'Markiert ein Ziel als abgeschlossen.',
defaultPolicy: 'propose',
parameters: [{ name: 'goalId', type: 'string', description: 'ID des Ziels', required: true }],
},
// ── Contacts ──────────────────────────────────────────────
{
name: 'create_contact',
@ -520,6 +638,222 @@ export const AI_TOOL_CATALOG: readonly ToolSchema[] = [
defaultPolicy: 'auto',
parameters: [],
},
// ── Mood ──────────────────────────────────────────────────
{
name: 'log_mood',
module: 'mood',
description:
'Erfasst einen Mood-Check-in mit Level (1-10), primaerer Emotion und optionalem Kontext.',
defaultPolicy: 'propose',
parameters: [
{
name: 'level',
type: 'number',
description: 'Stimmungs-Level von 1 (schlecht) bis 10 (super)',
required: true,
},
{
name: 'emotion',
type: 'string',
description: 'Primaere Emotion',
required: true,
enum: [
'happy',
'calm',
'energized',
'grateful',
'excited',
'loved',
'hopeful',
'neutral',
'bored',
'tired',
'sad',
'anxious',
'angry',
'stressed',
'frustrated',
'overwhelmed',
],
},
{
name: 'activity',
type: 'string',
description: 'Was machst du gerade?',
required: false,
enum: [
'work',
'exercise',
'social',
'alone',
'commute',
'eating',
'resting',
'creative',
'outdoors',
'screen',
'chores',
'other',
],
},
{
name: 'notes',
type: 'string',
description: 'Optionale Notiz zum Check-in',
required: false,
},
],
},
{
name: 'get_mood_today',
module: 'mood',
description: 'Gibt alle heutigen Mood-Eintraege zurueck mit Durchschnitts-Level und Emotionen.',
defaultPolicy: 'auto',
parameters: [],
},
{
name: 'get_mood_insights',
module: 'mood',
description:
'Gibt Mood-Trends und Muster zurueck: Durchschnitt der letzten 7/30 Tage, haeufigste Emotion, Positiv/Negativ-Verhaeltnis, und welche Aktivitaeten mit guter/schlechter Stimmung korrelieren.',
defaultPolicy: 'auto',
parameters: [
{
name: 'days',
type: 'number',
description: 'Analyse-Zeitraum in Tagen (Standard: 7)',
required: false,
},
],
},
// ── Finance ───────────────────────────────────────────────
{
name: 'add_transaction',
module: 'finance',
description: 'Erfasst eine Einnahme oder Ausgabe',
defaultPolicy: 'propose',
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 },
{
name: 'date',
type: 'string',
description: 'Datum (YYYY-MM-DD, Standard: heute)',
required: false,
},
],
},
{
name: 'get_month_summary',
module: 'finance',
description:
'Gibt die Finanz-Zusammenfassung fuer einen Monat zurueck: Einnahmen, Ausgaben, Bilanz, Ausgaben pro Kategorie.',
defaultPolicy: 'auto',
parameters: [
{
name: 'month',
type: 'string',
description: 'Monat im Format YYYY-MM (Standard: aktueller Monat)',
required: false,
},
],
},
{
name: 'list_transactions',
module: 'finance',
description:
'Listet die letzten Transaktionen auf. Optional nach Typ (income/expense) und Monat filterbar.',
defaultPolicy: 'auto',
parameters: [
{
name: 'type',
type: 'string',
description: 'Nur income oder expense zeigen',
required: false,
enum: ['income', 'expense'],
},
{
name: 'month',
type: 'string',
description: 'Monat im Format YYYY-MM',
required: false,
},
{
name: 'limit',
type: 'number',
description: 'Maximale Anzahl (Standard: 20)',
required: false,
},
],
},
// ── Times ─────────────────────────────────────────────────
{
name: 'start_timer',
module: 'times',
description: 'Startet einen Zeitmess-Timer mit optionaler Beschreibung und Projekt.',
defaultPolicy: 'propose',
parameters: [
{
name: 'description',
type: 'string',
description: 'Beschreibung der Taetigkeit',
required: false,
},
{
name: 'projectId',
type: 'string',
description: 'ID eines Projekts (aus list_projects)',
required: false,
},
],
},
{
name: 'stop_timer',
module: 'times',
description: 'Stoppt den laufenden Timer und speichert den Zeiteintrag.',
defaultPolicy: 'propose',
parameters: [],
},
{
name: 'get_timer_status',
module: 'times',
description: 'Gibt den Status des laufenden Timers zurueck (ob aktiv, Dauer, Beschreibung).',
defaultPolicy: 'auto',
parameters: [],
},
{
name: 'get_time_stats',
module: 'times',
description:
'Gibt Zeiterfassungs-Statistiken zurueck: Stunden heute, diese Woche, und Aufschluesselung nach Projekt.',
defaultPolicy: 'auto',
parameters: [
{
name: 'period',
type: 'string',
description: 'Zeitraum (Standard: week)',
required: false,
enum: ['today', 'week', 'month'],
},
],
},
{
name: 'list_projects',
module: 'times',
description: 'Listet alle aktiven Zeiterfassungs-Projekte mit Kunden-Info auf.',
defaultPolicy: 'auto',
parameters: [],
},
];
// ═══════════════════════════════════════════════════════════════