/** * bin/finance.ts — Personal Finance CLI */ import 'dotenv/config'; import { readFileSync, existsSync } from 'fs'; import { SimpleFINClient, saveAccessUrlToEnv } from '../server/finance/clients/SimpleFINClient.js'; import { PersonalFinanceAnalyzer } from '../server/finance/PersonalFinanceAnalyzer.js'; import { PortfolioAdvisor } from '../server/finance/PortfolioAdvisor.js'; import { ScreenerEngine } from '../server/screener/ScreenerEngine.js'; import { FinanceReporter } from '../server/reporters/FinanceReporter.js'; import type { PortfolioHolding } from '../server/types.js'; const PORTFOLIO_PATH = './portfolio.json'; async function main(): Promise { if (!existsSync(PORTFOLIO_PATH)) throw new Error('portfolio.json not found — edit it with your holdings and re-run.'); const { holdings } = JSON.parse(readFileSync(PORTFOLIO_PATH, 'utf8')) as { holdings: PortfolioHolding[]; }; const byType = holdings.reduce>((acc, h) => { const t = h.type ?? 'stock'; acc[t] = (acc[t] ?? 0) + 1; return acc; }, {}); console.log( `šŸ“‹ Portfolio: ${holdings.length} positions — ${Object.entries(byType) .map(([t, n]) => `${n} ${t}`) .join(', ')}\n`, ); // ── SimpleFIN accounts (optional) let personalFinance = null; if (process.env.SIMPLEFIN_ACCESS_URL || process.env.SIMPLEFIN_SETUP_TOKEN) { try { process.stdout.write('šŸ’° Fetching SimpleFIN accounts...'); const client = new SimpleFINClient({ onAccessUrlClaimed: saveAccessUrlToEnv }); await client.init(); const { accounts } = await client.getAccounts(); personalFinance = new PersonalFinanceAnalyzer().analyse(accounts); process.stdout.write(` ${accounts.length} accounts loaded\n`); } catch (err) { process.stdout.write(` skipped — ${(err as Error).message}\n`); } } else { console.log('ℹ Add SIMPLEFIN_SETUP_TOKEN to .env for account balances & spending data\n'); } // ── Screen stocks & ETFs const screenableTickers = holdings .filter((h) => (h.type ?? 'stock') !== 'crypto') .map((h) => h.ticker.toUpperCase()); let results = { STOCK: [] as any[], ETF: [] as any[], BOND: [] as any[], ERROR: [] as any[], marketContext: {} as any, }; if (screenableTickers.length > 0) { process.stdout.write(`šŸ“Š Screening ${screenableTickers.length} stock/ETF positions...`); results = (await new ScreenerEngine().screenTickers(screenableTickers)) as any; process.stdout.write(' done\n'); } process.stdout.write('šŸ’” Generating portfolio advice...'); const advice = await new PortfolioAdvisor().advise(holdings, results); process.stdout.write(' done\n'); const reportPath = new FinanceReporter().generate( advice as any, personalFinance, results.marketContext, ); console.log(`\nāœ… Finance report: ${reportPath}\n`); } main().catch((err) => { console.error('Failed:', (err as Error).message); process.exit(1); });