// Generated, do not edit. sha256:46e628fbaa9521b9e3fa070e20e19f0d90e6d2eab65f98990ca26652e15c420e import { CONFIG, ENUMS } from '../../shared/config.js'; import { unitToSubunit, unitPriceToBase } from '../../shared/units.js'; import { assertScope } from '../ai/gate.js'; import { limitedList } from '../ai/limits.js'; import { listAssets } from '../data/assets.js'; import { listAccounts } from '../data/accounts.js'; import { getBaseCurrency } from '../data/portfolio.js'; import { createAiProposal, listAiProposals } from '../data/ai-proposals.js'; import { vertalerVoor } from './session.mjs'; import { idVoorHandle } from './handles.mjs'; const NOTE_MAX = 500; const AFHANDELEN = 'De gebruiker vindt het voorstel in AssetApp onder Gebeurtenissen, "Af te handelen", ' + 'en keurt het daar goed of wijst het af. Er is nu nog niets gewijzigd.'; function onbekend(soort, gevraagd) { return { text: `De verwijzing '${gevraagd}' hoort bij geen ${soort} in deze administratie. ` + `Vraag de huidige verwijzingen op met admin_overview; mogelijk is de rij verwijderd of komt de ` + `verwijzing uit een andere installatie.`, fieldsShared: [], }; } function klaargezet(t, id, omschrijving) { const handle = t.toAi({ id }, { entity: 'proposal' }).id; return { text: [`Voorstel ${handle} staat klaar: ${omschrijving}`, AFHANDELEN].join('\n'), fieldsShared: t.fieldsShared(), }; } const BELOFTE = 'Wijzigt niets: legt een voorstel vast dat de eigenaar van de app goedkeurt of afwijst.'; const NOTE_SCHEMA = Object.freeze({ type: 'string', maxLength: NOTE_MAX, description: 'Waarom je dit voorstelt; de gebruiker leest het op de kaart.', }); const verwijzing = (soort, voorbeeld) => Object.freeze({ type: 'string', maxLength: 64, description: `De verwijzing naar ${soort} (${voorbeeld}).`, }); const MOMENT_SCHEMA = (wat) => Object.freeze({ type: 'string', maxLength: 32, description: `${wat}: 2026-03-01 of een ISO-tijdstempel.`, }); const SYMBOOL_SCHEMA = Object.freeze({ type: 'string', maxLength: 16, description: 'Het symbool van het asset zoals admin_overview het gaf: BTC, XAU, EUR.', }); function assetVoorSymbool(gevraagd) { const gezocht = String(gevraagd ?? '').trim().toUpperCase(); if (!gezocht) return null; return listAssets().find(a => String(a.symbol).toUpperCase() === gezocht) || null; } function onbekendSymbool(gevraagd) { const bestaat = listAssets().map(a => a.symbol).join(', '); return { text: `'${gevraagd}' is geen asset in deze administratie. Deze bestaan wel: ${bestaat}.`, fieldsShared: ['symbol'], }; } const stelBronkeuzeVoor = Object.freeze({ name: 'propose_source_choice', title: 'Een bronkeuze voorstellen', description: 'Stelt voor een koersbron van één asset als eerste te zetten voor spot of historie, of eruit te ' + 'halen; zie integration_health. ' + BELOFTE, inputSchema: Object.freeze({ type: 'object', properties: { symbol: SYMBOOL_SCHEMA, choice: { type: 'string', enum: ['spot', 'history'] }, provider: { type: 'string', maxLength: 60, description: 'De sleutel van de bron.' }, action: { type: 'string', enum: [...ENUMS.SOURCE_CHOICE_ACTION] }, note: NOTE_SCHEMA, }, required: ['symbol', 'choice', 'provider', 'action'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const asset = assetVoorSymbool(args?.symbol); if (!asset) return onbekendSymbool(args?.symbol); const id = await createAiProposal({ kind: 'source_choice', assetId: asset.id, scope: String(args?.choice ?? ''), key: String(args?.provider ?? ''), value: String(args?.action ?? ''), note: notitie(args?.note), }); const wat = args.action === 'first' ? 'als eerste zetten voor' : 'weghalen uit'; return klaargezet(t, id, `${args.provider} ${wat} de ${args.choice === 'spot' ? 'spot' : 'historie'} van ${asset.symbol}.`); }, }); const stelKoerspuntVoor = Object.freeze({ name: 'propose_price_point', title: 'Een koerspunt voorstellen', description: 'Stelt voor om één koers van één asset op één moment vast te leggen in AssetApp, voor een koers die ' + 'de app zelf niet kan ophalen. Wordt bij goedkeuring een handmatige koers op dagniveau. ' + BELOFTE, inputSchema: Object.freeze({ type: 'object', properties: { symbol: SYMBOOL_SCHEMA, fiatPrice: { type: 'number', exclusiveMinimum: 0, description: 'De koers in de rekenvaluta per hele eenheid (per BTC, per ozt goud).', }, observedAt: MOMENT_SCHEMA('Wanneer deze koers gold'), note: NOTE_SCHEMA, }, required: ['symbol', 'fiatPrice', 'observedAt'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const asset = assetVoorSymbool(args?.symbol); if (!asset) return onbekendSymbool(args?.symbol); const id = await createAiProposal({ kind: 'price_point', assetId: asset.id, fiatPrice: unitPriceToBase(args.fiatPrice, asset), observedAt: String(args.observedAt), note: notitie(args?.note), }); return klaargezet(t, id, `een koers voor ${asset.symbol} op ${args.observedAt}.`); }, }); const stelRekeningVoor = Object.freeze({ name: 'propose_account', title: 'Een account voorstellen', description: 'Stelt voor om een nieuw account aan te maken in AssetApp: de plek waar bezit wordt aangehouden. ' + BELOFTE + ' Een boeking kan pas naar dit account verwijzen nádat het voorstel is goedgekeurd; ' + 'vraag daarna opnieuw admin_overview op om de verwijzing te krijgen.', inputSchema: Object.freeze({ type: 'object', properties: { name: { type: 'string', maxLength: 100, description: 'De naam zoals de gebruiker hem in de app wil zien, bijvoorbeeld "Kluis thuis".', }, accountKind: { type: 'string', enum: [...ENUMS.ACCOUNT_KIND], description: 'vault is een kluis voor edelmetaal.', }, ownCustody: { type: 'boolean', description: 'True als de gebruiker het bezit zelf beheert, false als een derde het voor hem houdt.', }, note: NOTE_SCHEMA, }, required: ['name', 'accountKind', 'ownCustody'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const id = await createAiProposal({ kind: 'account', name: String(args?.name ?? ''), accountKind: String(args?.accountKind ?? ''), isOwnCustody: !!args?.ownCustody, note: notitie(args?.note), }); return klaargezet(t, id, `een nieuw account van de soort ${args.accountKind}.`); }, }); const stelBoekingVoor = Object.freeze({ name: 'propose_event', title: 'Een boeking voorstellen', description: 'Stelt één boeking voor. buy en interest: alleen toAccount; sell: alleen fromAccount; transfer: ' + 'allebei. buy en sell vragen fiatPrice. ' + BELOFTE, inputSchema: Object.freeze({ type: 'object', properties: { symbol: SYMBOOL_SCHEMA, eventType: { type: 'string', enum: [...ENUMS.LEDGER_EVENT_TYPE], description: 'transfer is tussen twee eigen accounts.', }, amount: { type: 'number', exclusiveMinimum: 0, description: 'De hoeveelheid in de HELE eenheid (0.25 = een kwart bitcoin), niet in de kleinste.', }, occurredAt: MOMENT_SCHEMA('Wanneer de boeking plaatsvond'), fromAccount: verwijzing('het account waar het vandaan komt', 'account:7f3a9c'), toAccount: verwijzing('het account waar het heen gaat', 'account:7f3a9c'), fiatPrice: { type: 'number', exclusiveMinimum: 0, description: 'De koers in de rekenvaluta per hele eenheid op het moment van de boeking.', }, fiatFee: { type: 'number', minimum: 0, description: 'Kosten als bedrag in de rekenvaluta (niet per eenheid).', }, unitFee: { type: 'number', minimum: 0, description: 'Kosten in het asset zelf (hele eenheid), bovenop amount. Alleen bij sell en transfer.', }, note: NOTE_SCHEMA, }, required: ['symbol', 'eventType', 'amount', 'occurredAt'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const asset = assetVoorSymbool(args?.symbol); if (!asset) return onbekendSymbool(args?.symbol); const rekeningIds = listAccounts().map(a => a.id); let fromAccountId = null; let toAccountId = null; if (args?.fromAccount != null) { fromAccountId = idVoorHandle(t, 'account', args.fromAccount, rekeningIds); if (fromAccountId === null) return onbekend('account', args.fromAccount); } if (args?.toAccount != null) { toAccountId = idVoorHandle(t, 'account', args.toAccount, rekeningIds); if (toAccountId === null) return onbekend('account', args.toAccount); } const amount = unitToSubunit(args.amount, asset); const id = await createAiProposal({ kind: 'event', assetId: asset.id, eventType: String(args?.eventType ?? ''), amount, fiatPrice: args?.fiatPrice == null ? null : unitPriceToBase(args.fiatPrice, asset), fiatFee: args?.fiatFee == null ? null : Number(args.fiatFee), unitFee: args?.unitFee == null ? null : unitToSubunit(args.unitFee, asset), observedAt: String(args.occurredAt), fromAccountId, toAccountId, note: notitie(args?.note), }); return klaargezet(t, id, `een boeking van de soort ${args.eventType} op ${args.occurredAt}.`); }, }); function notitie(waarde) { return typeof waarde === 'string' && waarde.trim() ? waarde.slice(0, NOTE_MAX) : null; } const stelVerwijderingVoor = Object.freeze({ name: 'propose_account_deletion', title: 'Een account laten verwijderen', description: 'Stelt voor om een bestaand account uit AssetApp te verwijderen. Dit is ONOMKEERBAAR zodra de ' + 'eigenaar het goedkeurt, dus de app vraagt daar een aparte bevestiging voor en neemt het niet mee ' + 'in "alles goedkeuren". Een account waar boekingen, balansen of scans aan hangen kan niet weg; het ' + 'voorstel weigert dan bij het goedkeuren en zegt hoeveel er in de weg staat. ' + BELOFTE, inputSchema: Object.freeze({ type: 'object', properties: { account: verwijzing('het account dat weg mag', 'account:7f3a9c'), note: NOTE_SCHEMA, }, required: ['account'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const accountId = idVoorHandle(t, 'account', args?.account, listAccounts().map(a => a.id)); if (accountId === null) return onbekend('account', args?.account); const id = await createAiProposal({ kind: 'account_delete', accountId, note: notitie(args?.note), }); return klaargezet(t, id, `${args.account} verwijderen. Dit is onomkeerbaar.`); }, }); const stelPortefeuilleVoor = Object.freeze({ name: 'propose_portfolio', title: 'Een portefeuille voorstellen', description: 'Stelt voor om een nieuwe, LEGE portefeuille (administratie) aan te maken naast de huidige. Bedoeld ' + 'om een demonstratie- of testadministratie op te bouwen zonder de echte aan te raken. Kopieert ' + 'niets: er komt geen data en geen sleutel in. Wisselt ook niet, want dat is een herstart van de ' + 'app; die keuze blijft bij de eigenaar, en jouw voorstellen landen tot dan in de huidige ' + 'administratie. ' + BELOFTE, inputSchema: Object.freeze({ type: 'object', properties: { name: { type: 'string', maxLength: 60, description: 'De naam van de nieuwe portefeuille, zoals de eigenaar hem in de app ziet.', }, currency: { type: 'string', description: 'ECB-code van de rekenvaluta (USD); ligt daarna vast. Standaard die van de huidige.', }, note: NOTE_SCHEMA, }, required: ['name'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const currency = args?.currency ? String(args.currency).toUpperCase() : getBaseCurrency(); const id = await createAiProposal({ kind: 'portfolio', name: String(args?.name ?? ''), currency, note: notitie(args?.note), }); return klaargezet(t, id, `een nieuwe lege portefeuille met de naam '${args.name}', in ${currency}.`); }, }); const stelVoorkeurVoor = Object.freeze({ name: 'propose_preference', title: 'Een instelling voorstellen', description: 'Stelt voor één instelling van deze administratie te wijzigen: elke instelling die admin_overview ' + 'toont, dus ook thema, accent, de vormgeving (glas, gloed) en de timers. Niet de AI-schuiven zelf. Een ongeldige waarde wordt ' + 'bij het goedkeuren geweigerd. ' + BELOFTE, inputSchema: Object.freeze({ type: 'object', properties: { scope: { type: 'string', enum: [...CONFIG.AI.PROPOSABLE_PREF_SCOPES], description: 'De groep van de instelling.', }, key: { type: 'string', maxLength: 60, description: 'De sleutel zoals admin_overview hem toont, bijvoorbeeld currency.', }, value: { type: ['string', 'number', 'boolean'], description: 'De nieuwe waarde.', }, note: NOTE_SCHEMA, }, required: ['scope', 'key', 'value'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const id = await createAiProposal({ kind: 'preference', scope: String(args?.scope ?? ''), key: String(args?.key ?? ''), value: JSON.stringify(args?.value ?? null), note: notitie(args?.note), }); return klaargezet(t, id, `de instelling ${args.scope}.${args.key} op ${JSON.stringify(args.value)} zetten.`); }, }); const stelBalansVoor = Object.freeze({ name: 'propose_valuation', title: 'Een balans voorstellen', description: 'Stelt de STAND van één account in één asset op een peildatum voor. Dat is een ANKER: na goedkeuring ' + 'IS de stand deze balans plus de boekingen erna, dus een verkeerde balans corrigeert het grootboek ' + 'stil. Gebruik alleen een onafhankelijke bron (afschrift, handelsplatform, telling); een stand die ' + 'je uit je eigen voorstellen optelt, klopt per constructie en controleert niets. Vergt een koers in ' + 'de historie op die datum, anders blijft het voorstel staan met die reden. ' + BELOFTE, inputSchema: Object.freeze({ type: 'object', properties: { symbol: SYMBOOL_SCHEMA, account: verwijzing('het account waarvan dit de stand is', 'account:7f3a9c'), balance: { type: 'number', minimum: 0, description: 'De stand in de HELE eenheid (0.25 = een kwart bitcoin). Nul mag: leeg op die datum.', }, valuedOn: MOMENT_SCHEMA('De peildatum'), note: NOTE_SCHEMA, }, required: ['symbol', 'account', 'balance', 'valuedOn'], additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const asset = assetVoorSymbool(args?.symbol); if (!asset) return onbekendSymbool(args?.symbol); const accountId = idVoorHandle(t, 'account', args?.account, listAccounts().map(a => a.id)); if (accountId === null) return onbekend('account', args?.account); const balance = unitToSubunit(args.balance, asset); const id = await createAiProposal({ kind: 'valuation', assetId: asset.id, accountId, balance, observedAt: String(args.valuedOn), note: notitie(args?.note), }); return klaargezet(t, id, `de stand van ${args.account} in ${asset.symbol} op ${args.valuedOn}.`); }, }); const voorstelToestand = Object.freeze({ name: 'proposal_status', title: 'De toestand van een voorstel', description: 'Zegt of een voorstel is goedgekeurd, afgewezen of nog wacht, en waarom een uitvoering mislukte. ' + 'Verandert niets. Vraag dit vóór je een boeking voorstelt naar een pas voorgesteld account.', inputSchema: Object.freeze({ type: 'object', properties: { proposal: { type: 'string', maxLength: 32, description: 'De verwijzing die een propose_-tool teruggaf (proposal:7f3a9c). Weglaten voor alles wat nog openstaat.', }, }, additionalProperties: false, }), scope: 'write', async run(args, context) { assertScope('write'); const t = await vertalerVoor(context); t.beginCall(); const alle = listAiProposals(); const gevraagd = String(args?.proposal ?? '').trim(); if (!gevraagd) { const open = alle.filter(p => p.status === 'pending'); if (!open.length) { return { text: 'Er staat geen voorstel meer open; alles is afgehandeld.', fieldsShared: t.fieldsShared() }; } const { lines } = limitedList(open, p => `- ${voorstelHandle(t, p.id)} (${p.kind}): wacht op een oordeel.`); return { text: [`${open.length} voorstel(len) wachten nog op een oordeel:`, ...lines].join('\n'), fieldsShared: [...t.fieldsShared(), 'kind', 'status'], }; } const rij = alle.find(p => voorstelHandle(t, p.id) === gevraagd); if (!rij) { return { text: `'${gevraagd}' is geen voorstel dat ik terugvind. Ik kijk ${CONFIG.AI.PROPOSAL_PAGE_SIZE} voorstellen terug.`, fieldsShared: [], }; } const uitleg = { pending: 'wacht nog op een oordeel van de eigenaar.', accepted: 'is goedgekeurd en uitgevoerd.', rejected: 'is afgewezen en niet uitgevoerd.', }[rij.status] ?? `heeft een onbekende status (${rij.status}).`; const reden = rij.status === 'pending' && rij.detail ? ` Een eerdere poging om hem uit te voeren mislukte: ${rij.detail}` : ''; return { text: `Voorstel ${gevraagd} (${rij.kind}) ${uitleg}${reden}`, fieldsShared: [...t.fieldsShared(), 'kind', 'status', ...(reden ? ['detail'] : [])], }; }, }); const voorstelHandle = (t, id) => String(t.toAi({ id }, { entity: 'proposal' }).id); export const WRITE_TOOLS = Object.freeze([ stelKoerspuntVoor, stelRekeningVoor, stelBoekingVoor, stelBalansVoor, stelVerwijderingVoor, stelPortefeuilleVoor, stelVoorkeurVoor, stelBronkeuzeVoor, voorstelToestand, ]);