π§ Tools Registry
51 toolscheck_room_availabilityβ handlerControleer welke vergaderzalen vrij zijn tussen twee tijdstippen. Gebruik dit voor vragen als 'welke kamers zijn vrij van 10 tot 12', 'is er nog een zaal beschikbaar vandaag', 'welke ruimtes kan ik boeken'. Geeft een lijst van vrije en bezette kamers terug.
Handler source bekijken
export async function check_room_availability({ date, start_time, end_time } = {}) {
if (!start_time || !end_time) return 'Geef een begin- en eindtijd op (bijv. 10:00 en 11:00).';
const resolvedDate = resolveDate(date);
const start = `${resolvedDate} ${start_time}`;
const end = `${resolvedDate} ${end_time}`;
try {
const res = await fetch(`${GRAPH_LOCAL_URL}/api/rooms/availability`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start, end }),
signal: AbortSignal.timeout(15000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const rooms = data.rooms || [];
const free = rooms.filter(r => r.available);
const busy = rooms.filter(r => !r.available);
const dateLabel = resolvedDate === new Date().toISOString().slice(0, 10) ? 'vandaag' : resolvedDate;
let result = `Beschikbaarheid ${dateLabel} van ${start_time} tot ${end_time}:\n\n`;
if (free.length) {
result += `β
Vrij (${free.length}):\n`;
result += free.map(r => ` - ${r.name}`).join('\n') + '\n\n';
} else {
result += `β Geen vrije zalen in dit tijdslot.\n\n`;
}
if (busy.length) {
result += `π΄ Bezet (${busy.length}):\n`;
result += busy.map(r => {
const who = r.busyPeriods?.[0]?.subject ? ` (${r.busyPeriods[0].subject.trim()})` : '';
return ` - ${r.name}${who}`;
}).join('\n');
}
return result.trim();
} catch (e) {
return `Fout bij ophalen beschikbaarheid: ${e.message}`;
}
}book_roomβ handlerReserveer een vergaderzaal op een bepaalde datum en tijd. Gebruik dit als iemand een kamer wil boeken of reserveren. Na aanroep wordt om bevestiging gevraagd β gebruik confirm_room_booking om te bevestigen of cancel_room_booking om te annuleren.
Handler source bekijken
export async function book_room({ date, start_time, end_time, room_name, subject = 'Meeting' } = {}) {
if (!start_time || !end_time) return 'Geef een begin- en eindtijd op.';
if (!room_name) return 'Welke zaal wil je boeken?confirm_room_bookingβ handlerBevestig de wachtende zaalreservering en voer hem daadwerkelijk uit. Gebruik dit nadat book_room is aangeroepen en de gebruiker bevestigt.
Handler source bekijken
export async function confirm_room_booking() {
if (!existsSync(PENDING_BOOKING_FILE)) return 'Geen boeking in de wachtrij. Gebruik book_room om eerst een boeking klaar te zetten.';
let pending;
try { pending = JSON.parse(readFileSync(PENDING_BOOKING_FILE, 'utf-8')); } catch { return 'Fout bij lezen van de wachtende boeking.'; }
try {
const res = await fetch(`${GRAPH_LOCAL_URL}/api/rooms/book`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomEmail: pending.roomEmail, subject: pending.subject, start: pending.start, end: pending.end }),
signal: AbortSignal.timeout(20000),
});
const data = await res.json();
unlinkSync(PENDING_BOOKING_FILE);
if (data.success) {
let msg = `β
Geboekt: ${pending.roomName} van ${pending.start.split(' ')[1]} tot ${pending.end.split(' ')[1]}`;
if (data.warning) msg += `. Let op: ${data.warning}`;
return msg;
} else {
return `β Boeking mislukt: ${data.error || 'onbekende fout'}`;
}
} catch (e) {
return `Fout bij uitvoeren boeking: ${e.message}`;
}
}cancel_room_bookingβ handlerAnnuleer de wachtende zaalreservering zonder hem uit te voeren. Gebruik dit als de gebruiker zegt dat hij de boeking niet wil of annuleert.
Handler source bekijken
export async function cancel_room_booking() {
if (!existsSync(PENDING_BOOKING_FILE)) return 'Geen boeking in de wachtrij om te annuleren.';
let pending;
try { pending = JSON.parse(readFileSync(PENDING_BOOKING_FILE, 'utf-8')); } catch {}
unlinkSync(PENDING_BOOKING_FILE);
return `Boeking geannuleerd: ${pending?.roomName || 'zaal'} op ${pending?.start?.split(' ')[0] || 'onbekend'}.`;
}get_system_statusβ handlerHaal direct de huidige systeemstatus op: schijfruimte, geheugengebruik, aantal draaiende Docker containers, open poorten en uptime. Gebruik dit voor vragen over serverstatus, schijfruimte, RAM, of resources.
Handler source bekijken
export async function get_system_status() {
try {
const disk = execSync('df -h / /mnt/media 2>/dev/null || df -h /', { timeout: 3000 }).toString().trim();
const dockerCount = execSync('docker ps -q 2>/dev/null | wc -l', { timeout: 3000 }).toString().trim();
const portCount = execSync("ss -tlnp 2>/dev/null | grep LISTEN | wc -l", { timeout: 3000 }).toString().trim();
const uptime = execSync('uptime -p 2>/dev/null', { timeout: 2000 }).toString().trim();
const mem = execSync("free -h | awk '/^Mem/{print $3\"/\"$2}'", { timeout: 2000 }).toString().trim();
const diskLines = disk.split('\n').slice(1).map(line => {
const parts = line.split(/\s+/);
return `${parts[5] || parts[0]}: ${parts[3]} vrij (${parts[4]} gebruikt)`;
});
return [
`Schijfruimte: ${diskLines.join(', ')}`,
`Docker containers actief: ${dockerCount.trim()}`,
`Poorten in gebruik: ${portCount.trim()}`,
`RAM gebruik: ${mem}`,
`Uptime: ${uptime}`,
].join('\n');
} catch (e) {
return `Fout bij systeem status ophalen: ${e.message}`;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TOOL: get_recent_downloads
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββget_recent_downloadsβ handlerHaal de meest recent gedownloade series (via Sonarr) en films (via Radarr) op. Gebruik dit voor vragen als 'wat is er gedownload', 'welke serie is nieuw', 'wat is er recent binnengekomen'.
Handler source bekijken
export async function get_recent_downloads({ type = 'all', limit = 5 } = {}) {
const results = [];
try {
if (type === 'all' || type === 'series' || type === 'tv') {
const data = await fetchJson(
`${SONARR_URL}/api/v3/history?pageSize=${limit * 3}&sortKey=date&sortDirection=descending`,
{ 'X-Api-Key': SONARR_KEY }
);
for (const r of (data.records || []).filter(r => r.eventType === 'downloadFolderImported').slice(0, limit)) {
const title = r.sourceTitle?.replace(/\.(mkv|mp4|avi)$/i, '') || 'Onbekend';
const date = r.date ? new Date(r.date).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' }) : '?';
results.push(`πΊ ${title} β ${date}`);
}
}
if (type === 'all' || type === 'films' || type === 'movies') {
const data = await fetchJson(
`${RADARR_URL}/api/v3/history?pageSize=${limit * 3}&sortKey=date&sortDirection=descending`,
{ 'X-Api-Key': RADARR_KEY }
);
for (const r of (data.records || []).filter(r => r.eventType === 'downloadFolderImported').slice(0, limit)) {
const title = r.sourceTitle?.replace(/\.(mkv|mp4|avi)$/i, '') || r.movie?.title || 'Onbekend';
const date = r.date ? new Date(r.date).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' }) : '?';
results.push(`π¬ ${title} β ${date}`);
}
}
if (!results.length) return 'Geen recente downloads gevonden.';
return results.slice(0, limit).join('\n');
} catch (e) {
return `Fout bij downloads ophalen: ${e.message}`;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TOOL: get_download_history
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββget_download_historyβ handlerHaal download geschiedenis op uit qBittorrent, inclusief bestandsnaam, grootte en HOE LANG de download duurde. Gebruik dit specifiek voor vragen over downloadtijden, snelheid, of hoe lang iets duurde.
Handler source bekijken
export async function get_download_history({ limit = 5 } = {}) {
try {
const data = await fetchJson(
`${QB_URL}/api/v2/torrents/info?limit=${limit}&sort=completion_on&reverse=true`
);
if (!Array.isArray(data) || !data.length) return 'Geen download geschiedenis gevonden.';
const lines = data.slice(0, limit).map(t => {
const name = t.name?.substring(0, 50) || 'Onbekend';
const size = t.size ? `${(t.size / 1e9).toFixed(1)} GB` : '?';
const durationSec = t.completion_on && t.added_on ? t.completion_on - t.added_on : null;
const duration = durationSec
? durationSec < 60 ? `${durationSec}s`
: durationSec < 3600 ? `${Math.round(durationSec / 60)}min`
: `${(durationSec / 3600).toFixed(1)}u`
: '?';
const completed = t.completion_on ? new Date(t.completion_on * 1000).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' }) : '?';
return `β’ ${name} β ${size} in ${duration} (klaar: ${completed})`;
});
return `Recente downloads:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij download geschiedenis: ${e.message}`;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TOOL: get_azure_costs
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββget_azure_costsβ handlerSchat de Azure AI kosten voor de huidige dag op basis van het aantal voice sessies en transcript lengte. Gebruik dit voor vragen over kosten, prijs, of hoeveel de AI gesprekken kosten.
Handler source bekijken
export async function get_azure_costs() {
// Azure Cost Management API requires subscription ID + access token
// For now: estimate based on session count and known pricing
// gpt-realtime-mini pricing: ~$0.06/min audio input + $0.12/min audio output
try {
const transcriptDir = resolve(__dirname, '../transcripts');
const files = (await import('fs')).readdirSync(transcriptDir).filter(f => f.endsWith('.txt') && f.startsWith(new Date().toISOString().slice(0, 10)));
const sessionCount = files.length;
// Count transcript lines for time estimation
let totalLines = 0;
for (const f of files) {
const content = (await import('fs')).readFileSync(`${transcriptDir}/${f}`, 'utf-8');
totalLines += content.split('\n').filter(l => l.includes('USER:') || l.includes('AI:')).length;
}
const estMinutes = Math.max(1, totalLines * 0.3); // ~18s per exchange
const estCost = (estMinutes * 0.06 + estMinutes * 0.12).toFixed(2);
return [
`Vandaag: ${sessionCount} voice sessie(s)`,
`Geschatte duur: ~${estMinutes.toFixed(0)} minuten`,
`Geschatte kosten: ~$${estCost} USD (gpt-realtime-mini: $0.06/min input + $0.12/min output)`,
`Deployment: ${ENV.Aget_jellyfin_historyβ handlerHaal de meest recent bekeken films en series op uit Jellyfin. Gebruik dit voor vragen als 'wat heb ik gekeken', 'wat was de laatste film', 'mijn kijkgeschiedenis'.
Handler source bekijken
export async function get_jellyfin_history({ limit = 5 } = {}) {
try {
const data = await fetchJson(
`${JELLYFIN_URL}/Users/${JELLYFIN_USER}/Items?SortBy=DatePlayed&SortOrder=Descending&Filters=IsPlayed&Limit=${limit}&Recursive=true&Fields=Name,UserData,SeriesName,ProductionYear&IncludeItemTypes=Movie,Episode`,
{ 'X-MediaBrowser-Token': JELLYFIN_KEY }
);
const items = data.Items || [];
if (!items.length) return 'Geen recent bekeken items gevonden in Jellyfin.';
const lines = items.map(item => {
const name = item.SeriesName ? `${item.SeriesName} β ${item.Name}` : item.Name;
const year = item.ProductionYear ? ` (${item.ProductionYear})` : '';
const played = item.UserData?.LastPlayedDate
? new Date(item.UserData.LastPlayedDate).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })
: '?';
const type = item.Type === 'Movie' ? 'π¬' : 'πΊ';
return `${type} ${name}${year} β ${played}`;
});
return `Recent bekeken in Jellyfin:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij Jellyfin geschiedenis ophalen: ${e.message}`;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TOOL: get_orchestration_schedules
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββget_orchestration_schedulesβ handlerHaal de lijst van ingeplande agents op uit orchestration.local. Gebruik dit voor vragen als 'wat staat er ingepland', 'welke agents draaien automatisch', 'wat is er geconfigureerd in orchestration'.
Handler source bekijken
export async function get_orchestration_schedules() {
try {
const schedules = await fetchJson(`${ORCHESTRATION_URL}/api/schedules`);
if (!Array.isArray(schedules) || !schedules.length) return 'Geen geplande agents gevonden in orchestration.local.';
const lines = schedules.map(s => {
const status = s.enabled ? 'β
' : 'βΈοΈ';
const lastRun = s.lastRunAt
? new Date(s.lastRunAt).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })
: 'nog niet gedraaid';
return `${status} ${s.agentName} β ${s.recurrenceLabel} (laatste run: ${lastRun})`;
});
return `Ingeplande agents in orchestration.local:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij ophalen orchestration schedules: ${e.message}`;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Handler map β used by relay to dispatch tool calls
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββget_jellyfin_recent_watchedβ handlerHaalt de recent bekeken items op in Jellyfin (laatste 10 films/series)
Handler source bekijken
export async function get_jellyfin_recent_watched({ limit = 10 } = {}) {
try {
const data = await fetchJson(
`${JELLYFIN_URL}/Users/${JELLYFIN_USER}/Items?SortBy=DatePlayed&SortOrder=Descending&Filters=IsPlayed&Limit=${limit}&Recursive=true&Fields=Name,UserData,SeriesName,ProductionYear&IncludeItemTypes=Movie,Episode`,
{ 'X-MediaBrowser-Token': JELLYFIN_KEY }
);
const items = data.Items || [];
if (!items.length) return 'Geen recent bekeken items gevonden in Jellyfin.';
const lines = items.map(item => {
const name = item.SeriesName ? `${item.SeriesName} β ${item.Name}` : item.Name;
const year = item.ProductionYear ? ` (${item.ProductionYear})` : '';
const played = item.UserData?.LastPlayedDate
? new Date(item.UserData.LastPlayedDate).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })
: '?';
return `${item.Type === 'Movie' ? 'π¬' : 'πΊ'} ${name}${year} β ${played}`;
});
return `Recent bekeken in Jellyfin:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij ophalen Jellyfin kijkgeschiedenis: ${e.message}`;
}
}get_prowlarr_indexersβ handlerHaalt de lijst van indexers op uit Prowlarr met status
Handler source bekijken
export async function get_prowlarr_indexers() {
try {
const data = await fetchJson(`${PROWLARR_URL}/api/v1/indexer`, { 'X-Api-Key': PROWLARR_KEY });
if (!Array.isArray(data) || !data.length) return 'Geen indexers gevonden in Prowlarr.';
const lines = data.map(i => `${i.enable ? 'β
' : 'β'} ${i.name} (${i.protocol})`);
return `Prowlarr indexers:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij ophalen Prowlarr indexers: ${e.message}`;
}
}get_qbittorrent_recentβ handlerHaalt recente torrents op met naam, status, grootte en download tijd
Handler source bekijken
export async function get_qbittorrent_recent() {
try {
const data = await fetchJson(`${QB_URL}/api/v2/torrents/info?filter=completed&limit=10&sort=completion_on&reverse=true`);
if (!Array.isArray(data) || !data.length) return 'Geen recente torrents gevonden.';
const lines = data.slice(0, 10).map(t => {
const name = t.name?.substring(0, 50) || 'Onbekend';
const size = t.size ? `${(t.size / 1e9).toFixed(1)} GB` : '?';
const durationSec = t.completion_on && t.added_on ? t.completion_on - t.added_on : null;
const duration = durationSec
? durationSec < 3600 ? `${Math.round(durationSec / 60)}min` : `${(durationSec / 3600).toFixed(1)}u`
: '?';
const completed = t.completion_on
? new Date(t.completion_on * 1000).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })
: '?';
return `β’ ${name} β ${size} in ${duration} (${completed})`;
});
return `Recente torrents:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij ophalen qBittorrent: ${e.message}`;
}
}get_radarr_recent_moviesβ handlerHaalt de laatste 10 gedownloade films op uit Radarr
Handler source bekijken
export async function get_radarr_recent_movies({ limit = 10 } = {}) {
try {
const data = await fetchJson(
`${RADARR_URL}/api/v3/history?pageSize=${limit}&sortKey=date&sortDirection=descending&eventType=1`,
{ 'X-Api-Key': RADARR_KEY }
);
const records = data.records || [];
if (!records.length) return 'Geen recente films gevonden.';
const lines = records.map(r => {
const title = r.movie?.title || r.sourceTitle || 'Onbekend';
const year = r.movie?.year ? ` (${r.movie.year})` : '';
const quality = r.quality?.quality?.name || '?';
const date = r.date ? new Date(r.date).toLocaleString('nl-NL', { dateStyle: 'short' }) : '?';
return `π¬ ${title}${year} β ${quality} (${date})`;
});
return `Recente films in Radarr:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij ophalen Radarr films: ${e.message}`;
}
}get_sonarr_recent_episodesβ handlerHaalt de laatste 10 gedownloade afleveringen op uit Sonarr
Handler source bekijken
export async function get_sonarr_recent_episodes({ limit = 10 } = {}) {
try {
const data = await fetchJson(
`${SONARR_URL}/api/v3/history?pageSize=${limit}&sortKey=date&sortDirection=descending&eventType=1`,
{ 'X-Api-Key': SONARR_KEY }
);
const records = data.records || [];
if (!records.length) return 'Geen recente afleveringen gevonden.';
const lines = records.map(r => {
const series = r.series?.title || 'Onbekend';
const ep = r.episode
? `S${String(r.episode.seasonNumber).padStart(2, '0')}E${String(r.episode.episodeNumber).padStart(2, '0')}: ${r.episode.title}`
: '';
const quality = r.quality?.quality?.name || '?';
const date = r.date ? new Date(r.date).toLocaleString('nl-NL', { dateStyle: 'short' }) : '?';
return `πΊ ${series} ${ep} β ${quality} (${date})`;
});
return `Recente afleveringen in Sonarr:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij ophalen Sonarr afleveringen: ${e.message}`;
}
}get_orchestration_scheduled_jobsβ handlerHaalt de ingeplande agent jobs op van orchestration.local
Handler source bekijken
export async function get_orchestration_scheduled_jobs() {
try {
const schedules = await fetchJson(`${ORCHESTRATION_URL}/api/schedules`);
if (!Array.isArray(schedules) || !schedules.length) return 'Geen ingeplande jobs gevonden.';
const lines = schedules.map(s => {
const status = s.enabled ? 'β
' : 'βΈοΈ';
const lastRun = s.lastRunAt
? new Date(s.lastRunAt).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })
: 'nog niet gedraaid';
const runStatus = s.lastRunStatus ? ` [${s.lastRunStatus}]` : '';
return `${status} ${s.agentName} β ${s.recurrenceLabel} (${lastRun}${runStatus})`;
});
return `Ingeplande jobs:\n${lines.join('\n')}`;
} catch (e) {
return `Fout bij ophalen orchestration jobs: ${e.message}`;
}
}get_docker_container_statsβ handlerHaalt CPU en geheugen gebruik op van draaiende Docker containers
Handler source bekijken
export async function get_docker_container_stats() {
try {
const result = execSync(
"docker stats --no-stream --format 'table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}' | head -n 20",
{ timeout: 10000 }
).toString().trim();
return result || 'Geen container stats beschikbaar.';
} catch (e) {
return `Fout bij ophalen Docker stats: ${e.message}`;
}
}
// AUTO-GENERATED by analyzer on 2026-02-25get_azure_costs_date_rangeβ handlerHaal geschatte Azure AI kosten op voor een specifieke datum of datumbereik
Handler source bekijken
export async function get_azure_costs_date_range({ date } = {}) {
try {
const transcriptDir = '/home/ubuntu/realtime-voice/transcripts';
let targetDate;
if (date === 'today') {
targetDate = new Date().toISOString().split('T')[0];
} else if (date === 'yesterday') {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
targetDate = yesterday.toISOString().split('T')[0];
} else {
targetDate = date;
}
const files = execSync(`find ${transcriptDir} -name "${targetDate}*.txt" -type f`, { encoding: 'utf-8' }).trim().split('\n').filter(Boolean);
if (files.length === 0) {
return `Geen transcripts gevonden voor ${targetDate}`;
}
let totalMinutes = 0;
let sessionCount = files.length;
for (const file of files) {
const content = execSync(`cat "${file}"`, { encoding: 'utf-8' });
const lines = content.split('\n').filter(l => l.includes('USER:') || l.includes('AI:'));
const duration = lines.length * 0.5;
totalMinutes += duration;
}
const inputCost = (totalMinutes * 60 * 0.06) / 1000000;
const outputCost = (totalMinutes * 60 * 0.24) / 1000000;
const totalCost = inputCost + outputCost;
return `Datum: ${targetDate}\nSessies: ${sessionCount}\nGeschatte duur: ${Math.round(totalMinutes)} minuten\nGeschatte kosten: $${totalCost.toFixed(2)} USD`;
} catch (e) {
return 'Fout bij ophalen kosten: ' + e.message;
}
}
// AUTO-GENERATED by analyzer on 2026-02-25get_qbittorrent_torrent_detailsβ handlerHaal gedetailleerde informatie op over recente torrents inclusief download tijd en snelheid
Handler source bekijken
export async function get_qbittorrent_torrent_details() {
try {
const torrents = await fetchJson(QB_URL + '/api/v2/torrents/info?filter=completed&limit=5');
if (!torrents || torrents.length === 0) {
return 'Geen voltooide torrents gevonden';
}
const result = torrents.map(t => {
const completedOn = new Date(t.completion_on * 1000);
const addedOn = new Date(t.added_on * 1000);
const durationMs = completedOn - addedOn;
const durationMin = Math.round(durationMs / 60000);
const sizeGB = (t.size / (1024 ** 3)).toFixed(2);
const avgSpeed = t.size / (durationMs / 1000) / (1024 ** 2);
return `${t.name}\n Grootte: ${sizeGB} GB\n Duur: ${durationMin} minuten\n Gem. snelheid: ${avgSpeed.toFixed(1)} MB/s\n Voltooid: ${completedOn.toLocaleString('nl-NL')}`;
}).join('\n\n');
return result;
} catch (e) {
return 'Fout bij ophalen torrent details: ' + e.message;
}
}
// AUTO-GENERATED by analyzer on 2026-02-25compare_ai_hosting_costsβ handlerVergelijk kosten van verschillende AI hosting opties (OpenAI Realtime API vs GPU huren vs eigen hardware)
Handler source bekijken
export async function compare_ai_hosting_costs({ daily_minutes } = {}) {
try {
const monthlyMinutes = daily_minutes * 30;
const monthlyHours = monthlyMinutes / 60;
const realtimeInputTokens = monthlyMinutes * 60 * 1000;
const realtimeOutputTokens = monthlyMinutes * 60 * 1000;
const realtimeCost = (realtimeInputTokens * 0.06 / 1000000) + (realtimeOutputTokens * 0.24 / 1000000);
const t4HourlyCost = 0.40;
const t4MonthlyCost = monthlyHours * t4HourlyCost;
const t4PurchaseCost = 1500;
const monthsToBreakEven = t4PurchaseCost / t4MonthlyCost;
return `Kostenvergelijking voor ${daily_minutes} min/dag (${monthlyMinutes} min/maand):\n\nOpenAI Realtime API:\n Maandelijks: $${realtimeCost.toFixed(2)}\n Jaarlijks: $${(realtimeCost * 12).toFixed(2)}\n\nNVIDIA T4 huren (cloud):\n Maandelijks: $${t4MonthlyCost.toFixed(2)}\n Jaarlijks: $${(t4MonthlyCost * 12).toFixed(2)}\n\nNVIDIA T4 kopen (~$1500):\n Break-even na: ${monthsToBreakEven.toFixed(1)} maanden\n Stroom/onderhoud: ~$50/maand extra\n\nAanbeveling: ${realtimeCost < t4MonthlyCost ? 'OpenAI Realtime API (goedkoper)' : 'Eigen GPU huren of kopen (goedkoper)'}`;
} catch (e) {
return 'Fout bij kostenvergelijking: ' + e.message;
}
}
// AUTO-GENERATED by analyzer on 2026-02-25get_system_service_countβ handlerTel alle draaiende services (Docker containers, systemd units, open poorten)
Handler source bekijken
export async function get_system_service_count() {
try {
const dockerContainers = execSync('docker ps --format "{{.Names}}"', { encoding: 'utf-8' }).trim().split('\n').filter(Boolean);
const systemdActive = execSync('systemctl list-units --type=service --state=running --no-pager --no-legend | wc -l', { encoding: 'utf-8' }).trim();
const listeningPorts = execSync('ss -tuln | grep LISTEN | wc -l', { encoding: 'utf-8' }).trim();
const dockerList = dockerContainers.join(', ');
return `Services overzicht:\n\nDocker containers: ${dockerContainers.length}\n ${dockerList}\n\nSystemd services: ${systemdActive}\nListening poorten: ${listeningPorts}`;
} catch (e) {
return 'Fout bij ophalen services: ' + e.message;
}
}
// AUTO-GENERATED by analyzer on 2026-02-25get_prowlarr_indexer_statsβ handlerHaal Prowlarr indexer statistieken op (welke indexers actief zijn, success rate, etc.)
Handler source bekijken
export async function get_prowlarr_indexer_stats() {
try {
const indexers = await fetchJson(PROWLARR_URL + '/api/v1/indexer', { 'X-Api-Key': PROWLARR_KEY });
if (!indexers || indexers.length === 0) {
return 'Geen indexers gevonden in Prowlarr';
}
const active = indexers.filter(i => i.enable);
const inactive = indexers.filter(i => !i.enable);
const result = `Prowlarr Indexers:\n\nActief: ${active.length}\nInactief: ${inactive.length}\nTotaal: ${indexers.length}\n\nActieve indexers:\n` +
active.map(i => ` - ${i.name} (${i.protocol})`).join('\n');
return result;
} catch (e) {
return 'Fout bij ophalen Prowlarr indexers: ' + e.message;
}
}get_notesβ handlerHaal de recente notities op die zijn opgeslagen via de voice agent. Gebruik dit voor vragen als 'welke notities heb ik', 'wat heb ik genoteerd', 'laat mijn notities zien', of 'notities van vandaag'.
Handler source bekijken
export async function get_notes({ limit = 10, category } = {}) {
try {
const notesFile = resolve(__dirname, '../notes/notes.json');
if (!existsSync(notesFile)) return 'Nog geen notities opgeslagen.';
let notes = JSON.parse(readFileSync(notesFile, 'utf-8'));
if (category) notes = notes.filter(n => n.category === category);
notes = notes.slice(0, limit);
if (!notes.length) return 'Geen notities gevonden' + (category ? ` voor categorie '${category}'` : '') + '.';
return notes.map(n => {
const date = new Date(n.timestamp).toLocaleString('nl-NL', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
return `[${date}] (${n.category}) ${n.text}`;
}).join('\n');
} catch (e) {
return 'Fout bij ophalen notities: ' + e.message;
}
}get_kanban_tasksβ handlerHaal open taken op van het Kanban bord (tasks.local). Gebruik dit voor vragen als 'welke taken heb ik open staan', 'wat staat er op het bord', 'backlog', of 'to-do taken'.
Handler source bekijken
export async function get_kanban_tasks({ column = 'all', limit = 10 } = {}) {
// Column mapping: graatje notes uses open/planning/bezig/klaar
const colMap = { 'backlog': 'open', 'todo': 'planning', 'in-progress': 'bezig', 'done': 'klaar' };
const resolvedCol = colMap[column] || column; // accept both old and new column names
try {
const url = resolvedCol !== 'all'
? `http://localhost:3031/api/notes?status=${resolvedCol}`
: 'http://localhost:3031/api/notes';
const data = await fetchJson(url);
let notes = data.notes || [];
// Default: exclude klaar unless specifically requested
if (resolvedCol === 'all') notes = notes.filter(n => (n.status ?? 'open') !== 'klaar');
notes = notes.slice(0, limit);
if (!notes.length) return 'Geen taken gevonden' + (resolvedCol !== 'all' ? ` met status '${resolvedCol}'` : '') + '.';
return notes.map(n => `[${(n.status ?? 'open').toUpperCase()}] ${n.text}${n.category ? ` (${n.category})` : ''}`).join('\n');
} catch (e) {
return 'Fout bij ophalen taken: ' + e.message;
}
}
// AUTO-GENERATED by analyzer on 2026-02-26get_whatsapp_recent_messagesβ handlerLees recente WhatsApp berichten uit de lokale database. Gebruik dit voor vragen als 'wat zijn mijn laatste WhatsApp berichten', 'heb ik nieuwe berichten', of 'wat staat er in WhatsApp'.
Handler source bekijken
export async function get_whatsapp_recent_messages({ limit = 10 } = {}) {
try {
const db = '/home/ubuntu/whatsapp-bot/data/whatsapp.db';
const result = execSync(
`sqlite3 -separator '|' ${db} "SELECT sender_jid, chat_jid, content, push_name, from_me, timestamp FROM wa_messages ORDER BY timestamp DESC LIMIT ${limit};"`
, { timeout: 5000 }
).toString().trim();
if (!result) return 'Geen recente berichten gevonden.';
return result.split('\n').map(line => {
const [senderJid, chatJid, content, pushName, fromMe, ts] = line.split('|');
const date = ts ? new Date(parseInt(ts) * 1000).toLocaleString('nl-NL', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '?';
const chatName = chatJid?.includes('g.us') ? '(groep)' : '(prive)';
const who = fromMe === '1' ? 'Ik' : (pushName || senderJid?.split('@')[0] || '?');
return `[${date}] ${chatName} ${who}: ${(content || '').slice(0, 120)}`;
}).join('\n');
} catch (e) {
return 'Fout bij ophalen WhatsApp berichten: ' + e.message;
}
}
// AUTO-GENERATED by analyzer on 2026-02-26run_transcript_analyzerβ handlerStart de transcript-analyzer die nieuwe gesprekken analyseert en automatisch tools genereert. Gebruik dit wanneer de gebruiker vraagt 'analyseer transcripts', 'genereer nieuwe tools', of 'trap de analyzer af'.
Handler source bekijken
export async function run_transcript_analyzer({ all = false } = {}) {
try {
mkdirSync(resolve(__dirname, '../jobs/logs'), { recursive: true });
const jobId = Date.now().toString();
const logFile = resolve(__dirname, `../jobs/logs/${jobId}-analyzer.log`);
const cmd = `node ${resolve(__dirname, '../analyzer/analyze.mjs')}${all ? ' --all' : ''}`;
const { spawn } = await import('child_process');
const child = spawn('bash', ['-c', `${cmd} > ${logFile} 2>&1`], { detached: true, stdio: 'ignore' });
child.unref();
const jobs = existsSync(resolve(__dirname, '../jobs/jobs.json'))
? JSON.parse(readFileSync(resolve(__dirname, '../jobs/jobs.json'), 'utf-8')) : [];
jobs.unshift({ id: jobId, name: 'Transcript Analyzer', command: cmd, status: 'running', startedAt: new Date().toISOString(), logFile });
writeFileSync(resolve(__dirname, '../jobs/jobs.json'), JSON.stringify(jobs, null, 2));
return `Transcript-analyzer gestart als achtergrondtaak (ID: ${jobId}). Gebruik check_background_task met dit ID om de status te zien. Log: ${logFile}`;
} catch (e) {
return 'Fout bij starten analyzer: ' + e.message;
}
}
// AUTO-GENERATED by analyzer on 2026-02-26get_available_toolsβ handlerGeef direct de lijst van alle beschikbare tools/skills terug zonder de backend-agent. Gebruik dit voor vragen als 'welke tools heb je', 'wat kun je', 'lijst van skills', of 'welke tools zijn beschikbaar'.
Handler source bekijken
export async function get_available_tools({ search } = {}) {
try {
const registry = JSON.parse(readFileSync(resolve(__dirname, 'registry.json'), 'utf-8'));
let tools = registry.filter(t => t.name && t.description);
if (search) {
const q = search.toLowerCase();
tools = tools.filter(t => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q));
}
if (!tools.length) return 'Geen tools gevonden' + (search ? ` voor "${search}"` : '') + '.';
return `${tools.length} tools beschikbaar:\n` + tools.map(t => `β’ ${t.name}: ${t.description.slice(0, 100)}`).join('\n');
} catch (e) {
return 'Fout bij ophalen tools: ' + e.message;
}
}
// Fixed: wa-cli does not exist β use HTTP API, always send to self for testingsend_whatsapp_to_numberβ handlerVerstuur WhatsApp bericht. Voor testdoeleinden wordt altijd naar jezelf (de eigenaar) gestuurd.
Handler source bekijken
export async function send_whatsapp_to_number({ phone_number, message } = {}) {
if (!message) return 'Fout: message is vereist';
const selfJid = ENV.WHATSAPP_SELF_JID || '31657856919@s.whatsapp.net';
try {
const res = await fetch('http://localhost:3022/api/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jid: selfJid, text: message }),
signal: AbortSignal.timeout(8000),
});
const result = await res.json();
if (result?.sent) return `WhatsApp bericht verstuurd (test β naar jezelf): "${message}"`;
return `Fout bij versturen: ${JSON.stringify(result)}`;
} catch (e) {
return 'Fout bij versturen WhatsApp: ' + e.message;
}
}
// Fixed: wa-cli does not exist β use HTTP APIsearch_whatsapp_contactsβ handlerZoek WhatsApp contacten op naam
Handler source bekijken
export async function search_whatsapp_contacts({ search_term } = {}) {
if (!search_term) return 'Geef een zoekterm op.';
try {
const res = await fetch('http://localhost:3022/api/wa/contacts', { signal: AbortSignal.timeout(5000) });
const contacts = await res.json();
const matches = contacts.filter(c =>
(c.name || '').toLowerCase().includes(search_term.toLowerCase()) ||
(c.notify || '').toLowerCase().includes(search_term.toLowerCase()) ||
(c.jid || '').includes(search_term)
);
if (!matches.length) return `Geen contacten gevonden voor: ${search_term}`;
return matches.map(c => `${c.name || c.notify || 'Onbekend'} β ${c.jid}`).join('\n');
} catch (e) {
return 'Fout bij zoeken contacten: ' + e.message;
}
}
// Fixed: wa-cli does not exist β use HTTP APIget_whatsapp_conversationsβ handlerHaal recente WhatsApp conversaties op
Handler source bekijken
export async function get_whatsapp_conversations({ limit = 10 } = {}) {
try {
const res = await fetch('http://localhost:3022/api/wa/chats', { signal: AbortSignal.timeout(5000) });
const chats = await res.json();
const top = Array.isArray(chats) ? chats.slice(0, limit) : [];
if (!top.length) return 'Geen conversaties gevonden.';
return top.map(c => `${c.name || c.jid} β ${c.unreadCount || 0} ongelezen`).join('\n');
} catch (e) {
return 'Fout bij ophalen conversaties: ' + e.message;
}
}get_torrent_download_statusβ handlerHaalt de downloadvoortgang op van torrents in qBittorrent. Optioneel zoeken op naam. Geeft percentage, status en downloadsnelheid terug.
Handler source bekijken
export async function get_torrent_download_status({ name } = {}) {
try {
const data = await fetchJson(`${QB_URL}/api/v2/torrents/info`);
if (!Array.isArray(data)) return 'Kon torrent lijst niet ophalen.';
if (name) {
const q = name.toLowerCase();
const match = data.find(t => t.name?.toLowerCase().includes(q));
if (!match) return `Geen torrent gevonden met naam "${name}". Actieve downloads: ${data.filter(t => t.progress < 1).map(t => t.name).join(', ') || 'geen'}`;
const pct = Math.round((match.progress || 0) * 100);
const state = match.state || 'onbekend';
const speed = match.dlspeed ? `${(match.dlspeed / 1024 / 1024).toFixed(1)} MB/s` : 'n.v.t.';
return `${match.name}: ${pct}% klaar (${state}), snelheid: ${speed}`;
}
const active = data.filter(t => t.progress < 1);
if (!active.length) return 'Geen actieve downloads. ' + (data.length ? `${data.length} voltooide torrents aanwezig.` : '');
return active.map(t => `${t.name}: ${Math.round((t.progress || 0) * 100)}% (${t.state || '?'})`).join('\n');
} catch (e) {
return `Fout bij ophalen torrents: ${e.message}`;
}
}get_analyzer_planβ handlerToont het huidige pending plan van de transcript analyzer: welke gaps gevonden zijn en welke nieuwe tools voorgesteld worden. Gebruik dit om te zien wat de analyzer wil toevoegen voordat je goedkeurt.
Handler source bekijken
export async function get_analyzer_plan() {
const stateFile = resolve(__dirname, '../analyzer/state.json');
try {
const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
const plan = state.pendingPlan;
if (!plan) return 'Geen pending analyzer plan gevonden.';
if (plan.approvedAt) return `Plan al goedgekeurd op ${new Date(plan.approvedAt).toLocaleString('nl-NL')}. Voer analyze.mjs opnieuw uit voor een nieuw plan.`;
const lines = [
`π Analyzer plan van ${new Date(plan.date).toLocaleString('nl-NL')}:`,
``,
`Gaps gevonden: ${plan.gaps?.length || 0}`,
];
for (const g of (plan.gaps || []).slice(0, 3)) {
lines.push(` - ${g.description}`);
}
lines.push(``, `Voorgestelde tools (${plan.new_tools?.length || 0}):`);
for (const t of (plan.new_tools || [])) {
lines.push(` + ${t.name}: ${t.description?.slice(0, 70)}`);
}
lines.push(``, `Goedkeuren?approve_analyzer_planβ handlerKeurt het pending analyzer plan goed en voert het uit: past de voorgestelde tools toe op handlers.mjs en registry.json, en herstart de relay. Gebruik get_analyzer_plan eerst om te zien wat er wordt toegepast.
Handler source bekijken
export async function approve_analyzer_plan() {
const analyzerScript = resolve(__dirname, '../analyzer/analyze.mjs');
const handlersPath = resolve(__dirname, 'handlers.mjs');
const serverPath = resolve(__dirname, '../server.mjs');
const stateFile = resolve(__dirname, '../analyzer/state.json');
try {
const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
if (!state.pendingPlan?.new_tools?.length) return 'Geen pending plan om goed te keuren.';
if (state.pendingPlan.approvedAt) return `Plan is al goedgekeurd op ${new Date(state.pendingPlan.approvedAt).toLocaleString('nl-NL')}.`;
// Run analyze --apply, then syntax-check both key files, then restart relay
const command = [
`node ${analyzerScript} --apply`,
`node --check ${handlersPath} && echo "β
handlers.mjs OK"`,
`node --check ${serverPath} && echo "β
server.mjs OK"`,
`pm2 restart realtime-voice`,
`sleep 3`,
`curl -sf http://127.0.0.1:3024/health | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8')); console.log('β
Health OK β', d.tools?.length ?? 0, 'tools')"`,
].join(' && ');
return await start_background_task({
name: 'Analyzer plan toepassen + valideren',
command,
});
} catch (e) {
return `Fout bij goedkeuren plan: ${e.message}`;
}
}execute_job_planβ handlerVoert het opgeslagen plan van een plan-modus Copilot taak uit. Gebruik na start_copilot_task met plan_only:true β leest het plan van die taak en voert het stap voor stap uit.
Handler source bekijken
export async function execute_job_plan({ job_id } = {}) {
if (!job_id?.trim()) return 'Geef een job ID op.';
try {
const jobs = JSON.parse(readFileSync(JOBS_FILE, 'utf-8'));
const planJob = jobs.find(j => j.id === job_id || j.name?.toLowerCase().includes(job_id.toLowerCase()));
if (!planJob) return `Geen taak gevonden met ID of naam "${job_id}".`;
if (!planJob.planText) return `Taak "${planJob.name}" heeft geen plan opgeslagen. Was het een plan-modus taak?`;
if (planJob.mode !== 'plan') return `Taak "${planJob.name}" is geen plan-modus taak (mode: ${planJob.mode || 'execute'}).`;
const res = await fetch('http://localhost:3031/api/jobs/copilot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: `Uitvoeren: ${planJob.name}`,
prompt: planJob.prompt,
dir: planJob.dir || '/home/ubuntu',
model: planJob.model,
executePlanJobId: planJob.id,
}),
});
const data = await res.json();
if (!res.ok) return `Fout bij starten uitvoering: ${data.error || res.status}`;
return `Plan van "${planJob.name}" wordt nu uitgevoerd (ID: ${data.job.id}). Bekijk voortgang op graatje.local/jobs`;
} catch (err) {
return `Fout: ${err.message}`;
}
}
// AUTO-GENERATED by analyzer on 2026-02-26validate_relay_filesβ handlerValideert de syntax van server.mjs en handlers.mjs (node --check) en de JSON-structuur van registry.json. Gebruik dit na elke bestandswijziging om te controleren of alles correct is.
Handler source bekijken
export async function validate_relay_files() {
const files = {
'server.mjs': resolve(__dirname, '../server.mjs'),
'handlers.mjs': resolve(__dirname, 'handlers.mjs'),
'registry.json': resolve(__dirname, 'registry.json'),
};
const results = [];
for (const [name, path] of Object.entries(files)) {
try {
if (name.endsWith('.json')) {
JSON.parse(readFileSync(path, 'utf-8'));
results.push(`β
${name}: valid JSON`);
} else {
execSync(`node --check ${path}`, { stdio: 'pipe' });
results.push(`β
${name}: syntax OK`);
}
} catch (e) {
const msg = e.stderr?.toString().trim() || e.message;
results.push(`β ${name}: ${msg.split('\n')[0]}`);
}
}
return results.join('\n');
}ask_environment_agentβ handlerStel een complexe vraag aan de Copilot AI agent die volledige toegang heeft tot de linuxdev VM omgeving (kan bash uitvoeren, bestanden lezen, services beheren). Gebruik dit ALLEEN voor vragen die niet door de specifieke tools beantwoord kunnen worden, zoals configuraties, logs, projecten, of specifieke problemen.
Handler source bekijken
export async function ask_environment_agent({ query } = {}) {
if (!query?.trim()) return 'Geef een vraag op.';
return await start_copilot_task({ name: query.substring(0, 60), prompt: query, dir: '/home/ubuntu', model: 'gpt-4.1' });
}send_whatsapp_messageβ handlerStuur een WhatsApp bericht naar de eigenaar of een specifiek nummer. Gebruik dit wanneer gevraagd om een WhatsApp te sturen, een reminder te sturen via WhatsApp, of iemand te berichten via WhatsApp.
Handler source bekijken
export async function send_whatsapp_message(args = {}) {
const { message } = args;
if (!message) return 'Fout: message parameter is vereist';
// Always send to self for testing β never to third parties
const selfJid = ENV.WHATSAPP_SELF_JID || '31657856919@s.whatsapp.net';
try {
const res = await fetch('http://localhost:3022/api/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jid: selfJid, text: message }),
signal: AbortSignal.timeout(8000),
});
const result = await res.json();
if (result?.sent) return `WhatsApp bericht verstuurd naar jezelf: "${message}"`;
return `Fout bij versturen: ${JSON.stringify(result)}`;
} catch (e) {
return `Fout bij WhatsApp: ${e.message}`;
}
}get_proxmox_backup_infoβ handlerHaal informatie op over Proxmox backup jobs: configuratie, schema's en status. Gebruik dit voor vragen over backups, backup schema's of backup status van de homelab server.
Handler source bekijken
export async function get_proxmox_backup_info(args = {}) {
try {
// Read Proxmox backup job config from local vzdump config
const configPath = '/etc/pve/vzdump.cron';
let backupJobs = 'Geen backup jobs gevonden';
try {
const config = readFileSync(configPath, 'utf-8');
backupJobs = config.trim();
} catch {
// Try to get info via local Proxmox API (if running on PVE host)
try {
const result = execSync(
'pvesh get /nodes/$(hostname)/backup --output-format json 2>/dev/null || echo "[]"',
{ timeout: 5000, encoding: 'utf-8' }
);
const jobs = JSON.parse(result.trim() || '[]');
if (jobs.length) {
backupJobs = `${jobs.length} backup job(s) gevonden:\n` +
jobs.map(j => ` - ${j.id || j.vmid || 'job'}: ${j.schedule || 'geen schema'}`).join('\n');
}
} catch {
backupJobs = 'Geen toegang tot Proxmox backup info (niet op PVE host)';
}
}
return `Proxmox Backup Info:\n${backupJobs}`;
} catch (e) {
return `Fout bij ophalen backup info: ${e.message}`;
}
}add_noteβ handlerZet een notitie klaar voor bevestiging (slaat nog NIET op). Na add_note MOET je altijd confirm_note aanroepen nadat de gebruiker bevestigd heeft, of reject_note als ze annuleren.
Handler source bekijken
export async function add_note({ text, category = 'algemeen' } = {}) {
if (!text?.trim()) return 'Geen tekst opgegeven voor de notitie.';
const pendingFile = resolve(__dirname, '../notes/pending_note.json');
const pending = {
text: text.trim(),
category,
stagedAt: new Date().toISOString(),
};
writeFileSync(pendingFile, JSON.stringify(pending, null, 2));
const preview = text.trim().slice(0, 80) + (text.length > 80 ? 'β¦' : '');
return `Notitie klaarstaan: "${preview}" (categorie: ${category}). Bevestig je dat ik hem opslaat, of wil je hem annuleren?`;
}start_background_taskβ handlerStart een langlopende taak als achtergrondproces. Gebruik dit voor taken die meer dan 30 seconden kunnen duren. Geeft een job ID terug waarmee je later de status kunt opvragen.
Handler source bekijken
export async function start_background_task({ name, command, task_id } = {}) {
if (!name?.trim() || !command?.trim()) return 'Geef een naam en commando op.';
mkdirSync(JOBS_LOGS, { recursive: true });
const id = Date.now().toString();
const logFile = join(JOBS_LOGS, `${id}.log`);
const job = { id, name: name.trim(), command: command.trim(), type: 'bash', status: 'running', startedAt: new Date().toISOString(), ...(task_id ? { taskId: task_id } : {}) };
const jobs = loadJobs();
jobs.push(job);
writeFileSync(JOBS_FILE, JSON.stringify(jobs, null, 2));
// Spawn detached via bash -c, redirect output to log
const escaped = command.replace(/'/g, "'\\''");
const wrapper = `bash -c '${escaped}' > ${logFile} 2>&1`;
const { spawn } = await import('child_process');
const child = spawn('bash', ['-c', wrapper], { detached: true, stdio: 'ignore' });
const pid = child.pid;
child.unref();
// Update pid
const idx = jobs.findIndex(j => j.id === id);
if (idx >= 0) { jobs[idx].pid = pid; writeFileSync(JOBS_FILE, JSON.stringify(jobs, null, 2)); }
// Watch for completion β update status and fire notifications
child.on('close', async (code) => {
try {
const current = JSON.parse(readFileSync(JOBS_FILE, 'utf-8'));
const i = current.findIndex(j => j.id === id);
if (i >= 0) {
current[i].status = code === 0 ? 'done' : 'failed';
current[i].exitCode = code;
current[i].finishedAt = new Date().toISOString();
writeFileSync(JOBS_FILE, JSON.stringify(current, null, 2));
}
} catch {}
// Notify voice server
try {
await fetch('http://localhost:3024/api/notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jobId: id, name: name.trim(), status: code === 0 ? 'done' : 'failed' }),
signal: AbortSignal.timeout(2000),
});
} catch {}
// Move linked graatje note to klaar/open
if (task_id) {
const noteStatus = code === 0 ? 'klaar' : 'open';
try {
await fetch(`http://localhost:3031/api/notes?id=${task_id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: noteStatus }),
signal: AbortSignal.timeout(2000),
});
} catch {}
}
});
return `Taak "${name}" gestart (ID: ${id}). Bekijk voortgang op graatje.local/jobs`;
}check_background_taskβ handlerControleer de status van een eerder gestarte achtergrondtaak. Geeft status (running/done/failed), looptijd en de laatste regels van het logbestand terug.
Handler source bekijken
export async function check_background_task({ job_id } = {}) {
if (!job_id) return 'Geef een job ID of naam op.';
const jobs = loadJobs();
const job = jobs.find(j => j.id === job_id) ||
jobs.find(j => j.name.toLowerCase().includes(job_id.toLowerCase()));
if (!job) return `Geen taak gevonden met ID/naam "${job_id}". Gebruik list_background_tasks voor overzicht.`;
const logFile = join(JOBS_LOGS, `${job.id}.log`);
let logContent = '';
if (existsSync(logFile)) {
try {
const raw = readFileSync(logFile, 'utf-8');
// Return last 3000 chars β enough for most research outputs
logContent = raw.length > 3000 ? '...[afgekapt]...\n' + raw.slice(-3000) : raw;
logContent = logContent.trim();
} catch {}
}
const durationMs = new Date(job.finishedAt ?? new Date()).getTime() - new Date(job.startedAt).getTime();
const durationStr = durationMs < 60000 ? `${Math.round(durationMs/1000)}s` : `${Math.round(durationMs/60000)}m`;
const parts = [
`Taak: ${job.name}`,
`Status: ${job.status === 'running' ? 'π‘ Bezig' : job.status === 'done' ? 'β
Klaar' : 'β Mislukt'}`,
`Gestart: ${new Date(job.startedAt).toLocaleString('nl-NL')}`,
job.finishedAt ? `Klaar: ${new Date(job.finishedAt).toLocaleString('nl-NL')}` : '',
`Duur: ${durationStr}`,
];
if (job.summary) parts.push(`\nSamenvatting: ${job.summary}`);
if (logContent) parts.push(`\nOutput:\n${logContent}`);
else parts.push('(geen output)');
return parts.filter(Boolean).join('\n');
}list_background_tasksβ handlerGeef een overzicht van alle achtergrondtaken: lopende, voltooide en mislukte taken. Handig als de gebruiker vraagt naar eerdere taken.
Handler source bekijken
export async function list_background_tasks() {
const jobs = loadJobs().reverse();
if (!jobs.length) return 'Geen achtergrondtaken gevonden.';
const running = jobs.filter(j => j.status === 'running');
const done = jobs.filter(j => j.status === 'done');
const failed = jobs.filter(j => j.status === 'failed');
const fmt = (j) => {
const durationMs = new Date(j.finishedAt ?? new Date()).getTime() - new Date(j.startedAt).getTime();
const dur = durationMs < 60000 ? `${Math.round(durationMs/1000)}s` : `${Math.round(durationMs/60000)}m`;
const icon = j.status === 'running' ? 'π‘' : j.status === 'done' ? 'β
' : 'β';
const typeIcon = j.type === 'copilot' ? 'π€' : 'βοΈ';
return ` ${icon} ${typeIcon} ${j.name} (ID: ${j.id}, ${dur})`;
};
const lines = [`Achtergrondtaken (${jobs.length} totaal):`];
if (running.length) { lines.push(`\nActief (${running.length}):`); running.forEach(j => lines.push(fmt(j))); }
if (done.length) { lines.push(`\nKlaar (${done.length}):`); done.slice(0,5).forEach(j => lines.push(fmt(j))); }
if (failed.length) { lines.push(`\nMislukt (${failed.length}):`); failed.forEach(j => lines.push(fmt(j))); }
lines.push('\nBekijk details: graatje.local/jobs');
return lines.join('\n');
}start_copilot_taskβ handlerStart een Copilot AI agent taak op de achtergrond. Gebruik voor complexe redeneer- of implementatietaken die meerdere stappen vereisen. Gebruik plan_only:true om eerst een plan te maken zonder iets te implementeren.
Handler source bekijken
export async function start_copilot_task({ name, prompt, dir, model, plan_only, task_id } = {}) {
if (!name?.trim() || !prompt?.trim()) return 'Geef een naam en prompt op.';
try {
const res = await fetch('http://localhost:3031/api/jobs/copilot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim(), prompt: prompt.trim(), dir, model, planOnly: plan_only, taskId: task_id }),
});
const data = await res.json();
if (!res.ok) return `Fout bij starten: ${data.error || res.status}`;
const modeLabel = plan_only ? '(plan-modus β wacht op goedkeuring)' : '';
const devUrl = data.job?.devLocalUrl ? ` Terminal: ${data.job.devLocalUrl}` : '';
return `Copilot taak "${name}" gestart ${modeLabel}(ID: ${data.job.id}).${devUrl} Bekijk voortgang op graatje.local/jobs`;
} catch (err) {
return `Fout: ${err.message}`;
}
}read_job_outputβ handlerLees de volledige output van een voltooide achtergrondtaak. Gebruik dit als je de resultaten van een eerdere onderzoekstaak wilt gebruiken om een vraag te beantwoorden. Geeft de schone, leesbare output terug zonder interne metadata.
Handler source bekijken
export async function read_job_output({ job_id, max_chars = 5000 } = {}) {
if (!job_id) return 'Geef een job ID of naam op.';
const jobs = loadJobs();
const job = jobs.find(j => j.id === job_id) ||
jobs.find(j => j.name.toLowerCase().includes(String(job_id).toLowerCase()));
if (!job) return `Geen taak gevonden met ID/naam "${job_id}".`;
if (job.status === 'running') return `Taak "${job.name}" is nog bezig. Wacht tot hij klaar is.`;
const logFile = join(JOBS_LOGS, `${job.id}.log`);
if (!existsSync(logFile)) return `Geen output gevonden voor taak "${job.name}".`;
try {
const raw = readFileSync(logFile, 'utf-8');
// Strip internal copilot metadata lines, keep only real content
const cleaned = raw
.split('\n')
.filter(line => !line.startsWith('[copilot]') && !line.startsWith('[tool]') && !line.startsWith('[permission]') && line !== '---')
.join('\n')
.trim();
const output = cleaned.length > max_chars
? cleaned.slice(0, max_chars) + `\n...[${cleaned.length - max_chars} extra tekens niet getoond]`
: cleaned;
return `Output van taak "${job.name}" (${job.status}, ${new Date(job.startedAt).toLocaleString('nl-NL')}):\n\n${output}`;
} catch (e) {
return `Fout bij lezen output: ${e.message}`;
}
}web_searchβ handlerZoek op het web via SearXNG (self-hosted meta-zoekmachine). Gebruik dit om actuele informatie op te zoeken, nieuws te vinden, of iets op te zoeken dat je niet weet. Geeft een lijst van relevante URLs en beschrijvingen terug.
Handler source bekijken
export async function web_search({ query, max_results = 5 } = {}) {
if (!query?.trim()) return 'Geef een zoekopdracht op.';
try {
const url = `http://localhost:8070/search?q=${encodeURIComponent(query)}&format=json&language=auto`;
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`SearXNG HTTP ${res.status}`);
const data = await res.json();
const results = (data.results || []).slice(0, max_results);
if (!results.length) return 'Geen zoekresultaten gevonden.';
return results.map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.content || ''}`).join('\n\n');
} catch (e) {
return `Fout bij zoeken: ${e.message}`;
}
}fetch_pageβ handlerHaal de inhoud van een webpagina op als schone markdown tekst. Gebruik dit nadat je een URL hebt gevonden via web_search, om de volledige pagina-inhoud te lezen.
Handler source bekijken
export async function fetch_page({ url } = {}) {
if (!url?.trim()) return 'Geef een URL op.';
try {
const res = await fetch(`https://r.jina.ai/${url}`, {
headers: { 'Accept': 'application/json' },
signal: AbortSignal.timeout(20000),
});
if (!res.ok) throw new Error(`Jina HTTP ${res.status}`);
const data = await res.json();
const content = (data.data?.content || data.content || '').substring(0, 4000);
const title = data.data?.title || data.title || url;
return `# ${title}\n\n${content}`;
} catch (e) {
return `Fout bij ophalen pagina: ${e.message}`;
}
}run_researchβ handlerStart een deep research onderzoek naar een onderwerp. Gebruik dit wanneer de gebruiker vraagt 'doe research naar', 'zoek uit', 'onderzoek', 'research' of vergelijkbare formuleringen. Het rapport wordt automatisch gepubliceerd op research.local.
Handler source bekijken
export async function run_research({ topic } = {}) {
if (!topic?.trim()) return 'Geef een onderwerp op om te onderzoeken.';
try {
const slug = topic.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
const date = new Date().toISOString().slice(0, 10);
const outputFile = `/home/ubuntu/research/${date}-${slug}.md`;
const prompt = `Doe uitgebreid research naar het volgende onderwerp: ${topic}
Gebruik Tavily en Brave Search om meerdere bronnen te raadplegen.
Schrijf een gestructureerd rapport en sla het op als: ${outputFile}
Het rapport moet bevatten:
- Samenvatting
- Belangrijkste bevindingen
- Bronnenlijst
Gebruik de bash tool om het bestand te schrijven naar: ${outputFile}`;
const { start_copilot_task } = await import('./handlers.mjs');
const result = await start_copilot_task({
name: `research-${slug}`,
prompt,
dir: '/home/ubuntu',
model: 'claude-sonnet-4.5',
});
return `Research gestart naar "${topic}". ${result}\n\nHet rapport wordt automatisch gepubliceerd op research.local zodra het klaar is.`;
} catch (e) {
return `Fout bij starten research: ${e.message}`;
}
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// ROOM TOOLS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const GRAPH_LOCAL_URL = 'http://localhost:3033';
const PENDING_BOOKING_FILE = resolve(__dirname, 'pending_booking.json');
// Known rooms for fuzzy matching
const KNOWN_ROOMS = [
{ name: 'OL MST VC 0.1', aliases: ['0.1', 'begane grond', 'beneden', 'bg'], email: 'meetingroom.beganegrond@openline.nl' },
{ name: 'OL MST VC 1.0', aliases: ['1.0', 'etage 1', '1e', 'eerste'], email: 'VideoConferenceRoom.etage1@openline.nl' },
{ name: 'OL MST VC 2.0', aliases: ['2.0', 'etage 2', '2e', 'tweede'], email: 'VideoConferenceRoom.etage2@openline.nl' },
{ name: 'OL MST VC 3.0', aliases: ['3.0', 'etage 3', '3e', 'derde', 'webex'], email: 'VideoConferenceRoom.etage3@openline.nl' },
{ name: 'OL MST VC 4.0', aliases: ['4.0', 'etage 4', '4e', 'vierde'], email: 'VideoConferenceRoom.etage4@openline.nl' },
];
function resolveDate(dateStr) {
if (!dateStr || dateStr === 'vandaag') return new Date().toISOString().slice(0, 10);
if (dateStr === 'morgen') {
const d = new Date(); d.setDate(d.getDate() + 1); return d.toISOString().slice(0, 10);
}
return dateStr;
}
function fuzzyMatchRoom(name) {
if (!name) return null;
const q = name.toLowerCase();
for (const room of KNOWN_ROOMS) {
if (room.name.toLowerCase().includes(q)) return room;
if (room.aliases.some(a => q.includes(a) || a.includes(q))) return room;
}
return null;
}confirm_noteβ handlerSla de eerder klaargezette notitie definitief op (na bevestiging van de gebruiker). Gebruik dit alleen nadat add_note is aangeroepen EN de gebruiker ja heeft gezegd.
Handler source bekijken
export async function confirm_note() {
const pendingFile = resolve(__dirname, '../notes/pending_note.json');
if (!existsSync(pendingFile)) return 'Geen notitie in de wachtrij om op te slaan.';
let pending;
try { pending = JSON.parse(readFileSync(pendingFile, 'utf-8')); } catch { return 'Fout bij lezen van de wachtende notitie.'; }
const notesFile = resolve(__dirname, '../notes/notes.json');
let notes = [];
try {
if (existsSync(notesFile)) notes = JSON.parse(readFileSync(notesFile, 'utf-8'));
} catch {}
const note = {
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
text: pending.text,
category: pending.category,
timestamp: new Date().toISOString(),
};
notes.unshift(note);
writeFileSync(notesFile, JSON.stringify(notes, null, 2));
unlinkSync(pendingFile);
return `Notitie opgeslagen: "${pending.text.slice(0, 60)}${pending.text.length > 60 ? 'β¦' : ''}"`;
}reject_noteβ handlerAnnuleer en verwijder de eerder klaargezette notitie (als de gebruiker nee zegt of annuleert). Gebruik dit alleen nadat add_note is aangeroepen EN de gebruiker nee heeft gezegd.
Handler source bekijken
export async function reject_note() {
const pendingFile = resolve(__dirname, '../notes/pending_note.json');
if (!existsSync(pendingFile)) return 'Geen notitie in de wachtrij om te verwijderen.';
let pending;
try { pending = JSON.parse(readFileSync(pendingFile, 'utf-8')); } catch {}
unlinkSync(pendingFile);
return `Notitie geannuleerd: "${pending?.text?.slice(0, 60) || '(onbekend)'}".`;
}
const JOBS_DIR = resolve(__dirname, '../jobs');
const JOBS_FILE = join(JOBS_DIR, 'jobs.json');
const JOBS_LOGS = join(JOBS_DIR, 'logs');
function loadJobs() {
if (!existsSync(JOBS_FILE)) return [];
try { return JSON.parse(readFileSync(JOBS_FILE, 'utf-8')); } catch { return []; }
}send_note_to_agentβ handlerStuur een bestaande notitie naar de Copilot agent als plan, research of autopilot taak. Gebruik note_id (van get_notes) of search (zoekterm in notitietekst). Mode: plan = alleen plannen, research = onderzoeken, autopilot = direct implementeren.
Handler source bekijken
export async function send_note_to_agent({ note_id, search, mode = 'autopilot' } = {}) {
if (!note_id && !search) return 'Geef een note_id of zoekterm op.';
if (!['plan', 'research', 'autopilot'].includes(mode)) return 'Ongeldige mode. Kies plan, research of autopilot.';
const notesFile = resolve(__dirname, '../notes/notes.json');
if (!existsSync(notesFile)) return 'Nog geen notities opgeslagen.';
let notes;
try { notes = JSON.parse(readFileSync(notesFile, 'utf-8')); } catch { return 'Fout bij lezen van notities.'; }
let note;
if (note_id) {
note = notes.find(n => n.id === note_id);
} else {
const term = search.toLowerCase();
note = notes.find(n => n.text.toLowerCase().includes(term));
}
if (!note) return `Geen notitie gevonden${note_id ? ` met ID "${note_id}"` : ` voor zoekterm "${search}"`}.`;
const prefixes = { plan: '[Plan]', research: '[Research]', autopilot: '[Autopilot]' };
const suffixes = {
plan: '\n\nAnalyseer dit en maak een stapsgewijs implementatieplan. Implementeer nog NIETS β alleen plannen.',
research: '\n\nDoe research naar dit onderwerp. Onderzoek de huidige staat en geef een beknopte analyse. Maak geen code-wijzigingen.',
autopilot: '\n\nVoer dit direct uit. Analyseer, plan, en implementeer in één sessie.',
};
const prompt = `${prefixes[mode]}\n${note.text}${suffixes[mode]}`;
const name = `${prefixes[mode]} ${note.text.slice(0, 50)}${note.text.length > 50 ? 'β¦' : ''}`;
try {
const res = await fetch('http://localhost:3031/api/jobs/copilot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, prompt, dir: '/home/ubuntu', planOnly: mode === 'plan' }),
});
const data = await res.json();
if (!res.ok) return `Fout bij starten: ${data.error || res.status}`;
const jobId = data.job?.id || data.jobId;
if (jobId) {
// Persist sentJobId/sentMode back to the note via graatje API
await fetch(`http://localhost:3031/api/notes?id=${note.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sentJobId: jobId, sentMode: mode }),
}).catch(() => {});
}
const modeLabel = { plan: 'gepland', research: 'onderzoek gestart', autopilot: 'autopilot gestart' }[mode];
const devUrl = data.job?.devLocalUrl ? ` Terminal: ${data.job.devLocalUrl}` : '';
return `Notitie "${note.text.slice(0, 60)}β¦" β ${modeLabel} (job ID: ${jobId}).${devUrl} Volg op graatje.local/jobs`;
} catch (err) {
return `Fout: ${err.message}`;
}
}get_whatsapp_last_by_unique_contactsβ handlerHaal de laatste berichten op van N unieke contacten (laatst ontvangen bericht per persoon)
Handler source bekijken
export async function get_whatsapp_last_by_unique_contacts({ count = 5 } = {}) {
try {
const messages = await fetchJson('http://localhost:3007/api/whatsapp/recent');
if (!messages || !Array.isArray(messages)) return 'Geen berichten beschikbaar';
const uniqueContacts = new Map();
for (const msg of messages) {
if (!uniqueContacts.has(msg.from) && msg.direction === 'incoming') {
uniqueContacts.set(msg.from, msg);
if (uniqueContacts.size >= count) break;
}
}
const result = Array.from(uniqueContacts.values()).map(msg =>
`Van: ${msg.fromName || msg.from}\nBericht: ${msg.body}\nTijd: ${msg.timestamp}`
).join('\n\n');
return result || 'Geen unieke contacten gevonden';
} catch (e) {
return 'Fout bij ophalen WhatsApp berichten: ' + e.message;
}
}