EXTENSION for TreeOS
finance
Personal finance. Track accounts, log transactions in natural language, monitor spending by category. Each account is a node with a balance. Say what you spent or earned and the tree updates. Budget goals per category. The AI reflects on patterns, flags overspending, helps you think through decisions. Cross-domain: knows your food spending, gym membership status, how finances relate to your whole life. Type 'be' for a guided check-in on your financial health.
v1.0.1 by TreeOS Site 0 downloads 9 files 779 lines 26.3 KB published 38d ago
treeos ext install finance
View changelog

Manifest

Provides

  • 1 CLI commands

Requires

  • services: hooks, llm, metadata
  • models: Node, Note

Optional

  • extensions: transactions, channels, html-rendering, treeos-base
SHA256: 4a931832dd072383d2e65c0c4595dec45a68b7d2561b153c5f1de541ceb89ed5

CLI Commands

CommandMethodDescription
fin [message...]POSTFinance. Log spending, check budgets, review.

Hooks

Listens To

  • enrichContext

Source Code

1/**
2 * Finance Core
3 *
4 * Track accounts, log transactions, monitor spending.
5 * The tree is the ledger.
6 */
7
8import log from "../../seed/log.js";
9import { setNodeMode } from "../../seed/modes/registry.js";
10
11let _Node = null;
12let _Note = null;
13let _metadata = null;
14
15export function configure({ Node, Note, metadata }) {
16  _Node = Node;
17  _Note = Note;
18  _metadata = metadata;
19}
20
21const ROLES = {
22  LOG: "log",
23  ACCOUNTS: "accounts",
24  CATEGORIES: "categories",
25  BUDGET: "budget",
26  PROFILE: "profile",
27  HISTORY: "history",
28};
29
30export { ROLES };
31
32// ── Scaffold ──
33
34export async function scaffold(rootId, userId) {
35  if (!_Node) throw new Error("Finance core not configured");
36  const { createNode } = await import("../../seed/tree/treeManagement.js");
37
38  const logNode = await createNode({ name: "Log", parentId: rootId, userId });
39  const accountsNode = await createNode({ name: "Accounts", parentId: rootId, userId });
40  const categoriesNode = await createNode({ name: "Categories", parentId: rootId, userId });
41  const budgetNode = await createNode({ name: "Budget", parentId: rootId, userId });
42  const profileNode = await createNode({ name: "Profile", parentId: rootId, userId });
43  const historyNode = await createNode({ name: "History", parentId: rootId, userId });
44
45  // Default accounts
46  const checking = await createNode({ name: "Checking", parentId: String(accountsNode._id), userId });
47  const savings = await createNode({ name: "Savings", parentId: String(accountsNode._id), userId });
48  const cash = await createNode({ name: "Cash", parentId: String(accountsNode._id), userId });
49  const creditCard = await createNode({ name: "Credit Card", parentId: String(accountsNode._id), userId });
50
51  // Default spending categories
52  for (const cat of ["Food", "Housing", "Transport", "Health", "Entertainment", "Shopping", "Bills", "Other"]) {
53    const catNode = await createNode({ name: cat, parentId: String(categoriesNode._id), userId });
54    await _metadata.setExtMeta(catNode, "finance", { role: "category" });
55  }
56
57  const tags = [
58    [logNode, ROLES.LOG],
59    [accountsNode, ROLES.ACCOUNTS],
60    [categoriesNode, ROLES.CATEGORIES],
61    [budgetNode, ROLES.BUDGET],
62    [profileNode, ROLES.PROFILE],
63    [historyNode, ROLES.HISTORY],
64  ];
65
66  for (const [node, role] of tags) {
67    await _metadata.setExtMeta(node, "finance", { role });
68  }
69
70  // Tag accounts with their type
71  for (const [node, type] of [[checking, "checking"], [savings, "savings"], [cash, "cash"], [creditCard, "credit"]]) {
72    await _metadata.setExtMeta(node, "finance", { role: "account", accountType: type });
73  }
74
75  await setNodeMode(rootId, "respond", "tree:finance-coach");
76  await setNodeMode(logNode._id, "respond", "tree:finance-log");
77
78  const root = await _Node.findById(rootId);
79  if (root) {
80    await _metadata.setExtMeta(root, "finance", {
81      initialized: true,
82      setupPhase: "complete",
83      currency: "USD",
84    });
85  }
86
87  const ids = {};
88  for (const [node, role] of tags) ids[role] = String(node._id);
89
90  log.info("Finance", `Scaffolded under ${rootId}`);
91  return ids;
92}
93
94// ── Find nodes ──
95
96export async function findFinanceNodes(rootId) {
97  if (!_Node) return null;
98  const children = await _Node.find({ parent: rootId }).select("_id name metadata").lean();
99  const result = {};
100  for (const child of children) {
101    const meta = child.metadata instanceof Map
102      ? child.metadata.get("finance")
103      : child.metadata?.finance;
104    if (meta?.role) result[meta.role] = { id: String(child._id), name: child.name };
105  }
106  return result;
107}
108
109export async function isInitialized(rootId) {
110  if (!_Node) return false;
111  const root = await _Node.findById(rootId).select("metadata").lean();
112  if (!root) return false;
113  const meta = root.metadata instanceof Map
114    ? root.metadata.get("finance")
115    : root.metadata?.finance;
116  return !!meta?.initialized;
117}
118
119export async function getSetupPhase(rootId) {
120  if (!_Node) return null;
121  const root = await _Node.findById(rootId).select("metadata").lean();
122  if (!root) return null;
123  const meta = root.metadata instanceof Map
124    ? root.metadata.get("finance")
125    : root.metadata?.finance;
126  return meta?.setupPhase || (meta?.initialized ? "complete" : null);
127}
128
129export async function completeSetup(rootId) {
130  const root = await _Node.findById(rootId);
131  if (!root) return;
132  const existing = _metadata.getExtMeta(root, "finance") || {};
133  await _metadata.setExtMeta(root, "finance", { ...existing, setupPhase: "complete" });
134}
135
136// ── Accounts ──
137
138export async function getAccounts(rootId) {
139  const nodes = await findFinanceNodes(rootId);
140  if (!nodes?.accounts) return [];
141
142  const children = await _Node.find({ parent: nodes.accounts.id })
143    .select("_id name metadata").lean();
144
145  return children.map(c => {
146    const meta = c.metadata instanceof Map
147      ? c.metadata.get("finance")
148      : c.metadata?.finance;
149    const values = c.metadata instanceof Map
150      ? c.metadata.get("values")
151      : c.metadata?.values;
152    return {
153      id: String(c._id),
154      name: c.name,
155      accountType: meta?.accountType || "other",
156      balance: values?.balance || 0,
157    };
158  });
159}
160
161// ── Categories ──
162
163export async function getCategories(rootId) {
164  const nodes = await findFinanceNodes(rootId);
165  if (!nodes?.categories) return [];
166
167  const children = await _Node.find({ parent: nodes.categories.id })
168    .select("_id name metadata").lean();
169
170  return children.map(c => {
171    const values = c.metadata instanceof Map
172      ? c.metadata.get("values")
173      : c.metadata?.values;
174    const goals = c.metadata instanceof Map
175      ? c.metadata.get("goals")
176      : c.metadata?.goals;
177    return {
178      id: String(c._id),
179      name: c.name,
180      spentThisMonth: values?.monthSpent || 0,
181      budget: goals?.monthBudget || 0,
182    };
183  });
184}
185
186// ── Spending summary ──
187
188export async function getMonthSummary(rootId) {
189  const accounts = await getAccounts(rootId);
190  const categories = await getCategories(rootId);
191
192  const totalBalance = accounts.reduce((sum, a) => sum + a.balance, 0);
193  const totalSpent = categories.reduce((sum, c) => sum + c.spentThisMonth, 0);
194  const totalBudget = categories.reduce((sum, c) => sum + c.budget, 0);
195
196  return {
197    accounts,
198    categories: categories.filter(c => c.spentThisMonth > 0 || c.budget > 0),
199    totalBalance,
200    totalSpent,
201    totalBudget,
202    budgetRemaining: totalBudget > 0 ? totalBudget - totalSpent : null,
203  };
204}
205
206// ── Recent transactions from log ──
207
208export async function getRecentTransactions(rootId, limit = 15) {
209  const nodes = await findFinanceNodes(rootId);
210  if (!nodes?.log) return [];
211
212  const { getNotes } = await import("../../seed/tree/notes.js");
213  const result = await getNotes({ nodeId: nodes.log.id, limit });
214  return result?.notes || [];
215}
216
1/**
2 * No data work needed. The AI handles everything via modes.
3 * Returns null so the orchestrator routes to the default mode.
4 */
5
6export async function handleMessage() {
7  return null;
8}
9
1import express from "express";
2import { sendError, ERR } from "../../seed/protocol.js";
3import urlAuth from "../html-rendering/urlAuth.js";
4import { htmlOnly } from "../html-rendering/htmlHelpers.js";
5import Node from "../../seed/models/node.js";
6import { isInitialized, getMonthSummary, getRecentTransactions } from "./core.js";
7import { renderFinanceDashboard } from "./pages/dashboard.js";
8
9const router = express.Router();
10
11router.get("/root/:rootId/finance", urlAuth, htmlOnly, async (req, res) => {
12  try {
13    const { rootId } = req.params;
14    const root = await Node.findById(rootId).select("name metadata").lean();
15    if (!root) return sendError(res, 404, ERR.TREE_NOT_FOUND, "Finance tree not found");
16
17    if (!(await isInitialized(rootId))) {
18      return sendError(res, 404, ERR.TREE_NOT_FOUND, "Finance not initialized");
19    }
20
21    const summary = await getMonthSummary(rootId);
22    const recentTransactions = await getRecentTransactions(rootId, 20);
23
24    res.send(renderFinanceDashboard({
25      rootId,
26      rootName: root.name,
27      summary,
28      recentTransactions,
29      token: req.query.token || null,
30      userId: req.user?._id?.toString() || req.user?.id || null,
31      inApp: !!req.query.inApp,
32    }));
33  } catch (err) {
34    const log = (await import("../../seed/log.js")).default;
35    log.error("Finance", `Dashboard error: ${err.message}`);
36    sendError(res, 500, ERR.INTERNAL, "Finance dashboard failed");
37  }
38});
39
40export default router;
41
1import log from "../../seed/log.js";
2import { configure, findFinanceNodes, getAccounts, getCategories, getMonthSummary, isInitialized, scaffold } from "./core.js";
3import { handleMessage } from "./handler.js";
4
5export async function init(core) {
6  configure({
7    Node: core.models.Node,
8    Note: core.models.Note,
9    metadata: core.metadata,
10  });
11
12  // ── Modes ──
13  const logMode = (await import("./modes/log.js")).default;
14  const coachMode = (await import("./modes/coach.js")).default;
15  const reviewMode = (await import("./modes/review.js")).default;
16
17  core.modes.registerMode(logMode.name, logMode, "finance");
18  core.modes.registerMode(coachMode.name, coachMode, "finance");
19  core.modes.registerMode(reviewMode.name, reviewMode, "finance");
20
21  // ── enrichContext: inject financial awareness into ALL tree nodes ──
22  const _financeCache = new Map(); // treeRootId -> { summary, ts }
23  const FINANCE_CACHE_TTL = 60000;
24
25  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
26    if (!node?._id) return;
27
28    // At finance nodes: full detail
29    const finMeta = meta?.finance;
30    if (finMeta?.role === "account") {
31      const values = meta?.values || {};
32      context.financeAccount = { name: node.name, type: finMeta.accountType, balance: values.balance || 0 };
33      return;
34    }
35    if (finMeta?.role === "category") {
36      const values = meta?.values || {};
37      const goals = meta?.goals || {};
38      context.financeCategory = { name: node.name, spent: values.monthSpent || 0, budget: goals.monthBudget || 0 };
39      return;
40    }
41
42    // For all other tree nodes: compact spending summary
43    const treeRootId = node.rootOwner ? String(node.rootOwner) : null;
44    if (!treeRootId) return;
45
46    const cached = _financeCache.get(treeRootId);
47    if (cached && Date.now() - cached.ts < FINANCE_CACHE_TTL) {
48      if (cached.summary) context.financeSummary = cached.summary;
49      return;
50    }
51
52    try {
53      const { getExtension } = await import("../loader.js");
54      const life = getExtension("life");
55      if (!life?.exports?.getDomainNodes) return;
56
57      const domains = await life.exports.getDomainNodes(treeRootId);
58      if (!domains.finance?.id) {
59        _financeCache.set(treeRootId, { summary: null, ts: Date.now() });
60        return;
61      }
62
63      const summary = await getMonthSummary(domains.finance.id);
64      const compact = {
65        totalBalance: summary.totalBalance,
66        spentThisMonth: summary.totalSpent,
67        budgetRemaining: summary.budgetRemaining,
68      };
69
70      _financeCache.set(treeRootId, { summary: compact, ts: Date.now() });
71      if (summary.totalBalance > 0 || summary.totalSpent > 0) {
72        context.financeSummary = compact;
73      }
74    } catch {}
75  }, "finance");
76
77  log.info("Finance", "Loaded. The tree tracks money.");
78
79  // ── Register apps-grid slot ──
80  try {
81    const { getExtension } = await import("../loader.js");
82    const base = getExtension("treeos-base");
83    base?.exports?.registerSlot?.("apps-grid", "finance", ({ userId, rootMap, tokenParam, tokenField, esc: e }) => {
84      const entries = rootMap.get("Finance") || [];
85      const existing = entries.map(entry =>
86        `<a class="app-active" href="/api/v1/root/${entry.id}/finance?html${tokenParam}" style="margin-right:8px;margin-bottom:6px;">${e(entry.name)}</a>`
87      ).join("");
88      return `<div class="app-card">
89        <div class="app-header"><span class="app-emoji">💰</span><span class="app-name">Finance</span></div>
90        <div class="app-desc">Track accounts, log spending in natural language. Budget goals per category. The AI reflects on patterns and helps you think about money.</div>
91        ${entries.length > 0
92          ? `<div style="display:flex;flex-wrap:wrap;">${existing}</div>`
93          : `<form class="app-form" method="POST" action="/api/v1/user/${userId}/apps/create">
94              ${tokenField}<input type="hidden" name="app" value="finance" />
95              <input class="app-input" name="message" placeholder="How much did you spend today?" required />
96              <button class="app-start" type="submit">Start Finance</button>
97            </form>`}
98      </div>`;
99    }, { priority: 60 });
100  } catch {}
101
102  // ── HTML dashboard route ──
103  let router = null;
104  try {
105    const { getExtension } = await import("../loader.js");
106    const htmlExt = getExtension("html-rendering");
107    if (htmlExt) {
108      router = (await import("./htmlRoutes.js")).default;
109    }
110  } catch {}
111
112  return {
113    router,
114    exports: {
115      scaffold,
116      isInitialized,
117      findFinanceNodes,
118      getAccounts,
119      getCategories,
120      getMonthSummary,
121      handleMessage,
122    },
123  };
124}
125
1export default {
2  name: "finance",
3  version: "1.0.1",
4  builtFor: "TreeOS",
5  description:
6    "Personal finance. Track accounts, log transactions in natural language, " +
7    "monitor spending by category. Each account is a node with a balance. " +
8    "Say what you spent or earned and the tree updates. Budget goals per category. " +
9    "The AI reflects on patterns, flags overspending, helps you think through " +
10    "decisions. Cross-domain: knows your food spending, gym membership status, " +
11    "how finances relate to your whole life. Type 'be' for a guided check-in " +
12    "on your financial health.",
13
14  territory: "money, spending, budget, income, savings, bills, rent, paycheck, debt, invest, account, bank, credit, debit, purchase, cost, price, afford",
15  classifierHints: [
16    /\$\d+/,                                                      // "$45", "$1200"
17    /\b\d+\s*(dollars?|bucks?|usd|eur|gbp)\b/i,                  // "45 dollars"
18    /\b(spent|paid|bought|cost|earned|received|deposited|withdrew|transferred|owe|owes)\b/i,
19    /\b(rent|mortgage|groceries|subscription|paycheck|salary|income|savings?|budget|invest|crypto|stock)\b/i,
20    /\b(electric|phone|water|internet|gas|utility|medical|cable)\s*bill\b/i,
21    /\b(bank|checking|credit card|debit|account|balance|net worth)\b/i,
22  ],
23
24  needs: {
25    models: ["Node", "Note"],
26    services: ["hooks", "llm", "metadata"],
27  },
28
29  optional: {
30    extensions: [
31      "transactions",
32      "channels",
33      "html-rendering",
34      "treeos-base",
35    ],
36  },
37
38  provides: {
39    models: {},
40    routes: false,
41    tools: false,
42    jobs: false,
43    modes: true,
44
45    hooks: {
46      fires: [],
47      listens: ["enrichContext"],
48    },
49
50    cli: [
51      {
52        command: "fin [message...]",
53        scope: ["tree"],
54        description: "Finance. Log spending, check budgets, review.",
55        method: "POST",
56        endpoint: "/root/:rootId/chat",
57        body: ["message"],
58      },
59    ],
60  },
61};
62
1import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findFinanceNodes, getAccounts, getCategories, getMonthSummary } from "../core.js";
3
4export default {
5  name: "tree:finance-coach",
6  emoji: "📊",
7  label: "Finance Coach",
8  bigMode: "tree",
9  hidden: true,
10
11  maxMessagesBeforeLoop: 6,
12  preserveContextOnLoop: true,
13
14  toolNames: [
15    "navigate-tree",
16    "get-tree-context",
17    "get-tree",
18    "get-node-notes",
19    "create-new-node",
20    "create-node-note",
21    "edit-node-note",
22    "edit-node-value",
23    "get-searched-notes-by-user",
24  ],
25
26  async buildSystemPrompt({ username, rootId, currentNodeId }) {
27    const finRoot = await findExtensionRoot(currentNodeId || rootId, "finance") || rootId;
28    const summary = finRoot ? await getMonthSummary(finRoot) : null;
29
30    const accountList = summary?.accounts?.length > 0
31      ? summary.accounts.map(a => `- ${a.name} (${a.accountType}): $${a.balance}`).join("\n")
32      : "No accounts set up.";
33
34    const categoryList = summary?.categories?.length > 0
35      ? summary.categories.map(c => {
36          const pct = c.budget > 0 ? ` (${Math.round((c.spentThisMonth / c.budget) * 100)}%)` : "";
37          return `- ${c.name}: $${c.spentThisMonth}${c.budget > 0 ? `/$${c.budget}` : ""}${pct}`;
38        }).join("\n")
39      : "";
40
41    const totalsBlock = summary
42      ? `Total balance: $${summary.totalBalance}\nSpent this month: $${summary.totalSpent}${summary.totalBudget > 0 ? `\nBudget remaining: $${summary.budgetRemaining}` : ""}`
43      : "";
44
45    const hasActivity = summary && (summary.totalBalance > 0 || summary.totalSpent > 0 || summary.totalBudget > 0);
46
47    return `You are ${username}'s financial coach.
48
49${hasActivity ? "STATUS: Accounts active." : "STATUS: Fresh start. Help them set up accounts and budgets."}
50
51${totalsBlock ? `FINANCIAL SNAPSHOT:\n${totalsBlock}\n` : ""}
52${accountList ? `ACCOUNTS:\n${accountList}\n` : ""}
53${categoryList ? `SPENDING THIS MONTH:\n${categoryList}\n` : ""}
54
55Your role: help ${username} think about money clearly. Budget setting, savings goals, spending awareness, debt strategy. You track and reflect. You don't predict markets or give investment advice.
56
57CAPABILITIES:
58- Set budget goals on category nodes (edit-node-value, key "monthBudget")
59- Create new accounts or categories
60- Review spending patterns from Log notes
61- Set account balances
62
63BEHAVIOR:
64- Be direct about numbers. Don't sugarcoat overspending.
65- When they ask "can I afford X", look at balances and upcoming obligations.
66- When they ask about budgets, show what they've spent vs. their goals.
67- Suggest concrete actions: "move $200 to savings" not "consider saving more."
68- If they ask about investments, stocks, crypto: help them think through the decision but be clear you track, you don't predict. Ask about their timeline, risk tolerance, and what they can afford to lose.
69- Cross-domain awareness: if you see food spending data, fitness membership costs, or other life context, use it naturally. "Your food spending is up 30% since you started bulking. That tracks."
70- Never expose node IDs or metadata to the user.`.trim();
71  },
72};
73
1import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findFinanceNodes, getAccounts, getCategories } from "../core.js";
3
4export default {
5  name: "tree:finance-log",
6  emoji: "💰",
7  label: "Finance Log",
8  bigMode: "tree",
9  hidden: true,
10
11  maxMessagesBeforeLoop: 6,
12  preserveContextOnLoop: true,
13
14  toolNames: [
15    "navigate-tree",
16    "get-tree-context",
17    "get-tree",
18    "get-node-notes",
19    "create-new-node",
20    "create-node-note",
21    "edit-node-note",
22    "edit-node-value",
23    "edit-node-goal",
24  ],
25
26  async buildSystemPrompt({ username, rootId, currentNodeId }) {
27    const finRoot = await findExtensionRoot(currentNodeId || rootId, "finance") || rootId;
28    const nodes = finRoot ? await findFinanceNodes(finRoot) : null;
29    const accounts = finRoot ? await getAccounts(finRoot) : [];
30    const categories = finRoot ? await getCategories(finRoot) : [];
31
32    const accountList = accounts.length > 0
33      ? accounts.map(a => `- ${a.name} (${a.accountType}): $${a.balance}`).join("\n")
34      : "No accounts yet.";
35
36    const categoryList = categories.length > 0
37      ? categories.map(c => {
38          const budgetPart = c.budget > 0 ? ` / $${c.budget} budget` : "";
39          return `- ${c.name}: $${c.spentThisMonth} this month${budgetPart}`;
40        }).join("\n")
41      : "Default categories available.";
42
43    const logId = nodes?.log?.id;
44    const accountsId = nodes?.accounts?.id;
45    const categoriesId = nodes?.categories?.id;
46
47    return `You are logging financial transactions for ${username}.
48
49ACCOUNTS:
50${accountList}
51
52CATEGORIES:
53${categoryList}
54
55The user tells you about money they spent, earned, or moved. Parse it and record it.
56
57WORKFLOW:
581. Parse the transaction: amount, what it was for, which account (default: Checking).
592. Write a note to Log (${logId || "find it"}) with: "$AMOUNT on DESCRIPTION from ACCOUNT".
603. Find the right category under Categories (${categoriesId || "find it"}). If none fits, use "Other" or create a new one.
614. Increment the category's monthSpent value: edit-node-value on the category node, key "monthSpent", amount spent.
625. Update the account balance: edit-node-value on the account node, key "balance", negative for spending, positive for income.
636. Confirm: "Spent $45 on groceries from Checking. Food: $57/$200 this month."
64
65PARSING RULES:
66- "spent $45 on groceries" = debit $45 from default account, category Food
67- "paid rent $1200" = debit $1200, category Housing
68- "got paid $2000" = credit $2000 to default account, category Income
69- "transferred $500 to savings" = debit Checking, credit Savings
70- If no amount given, ask.
71- If category is ambiguous, pick the closest match. Don't ask unless truly unclear.
72- Currency is USD unless configured differently.
73
74RULES:
75- One log note per transaction. Terse. "$45 groceries (Checking)"
76- Always update both the category spent AND the account balance.
77- Never expose node IDs or metadata to the user.
78- Confirm in one line with running totals.`.trim();
79  },
80};
81
1import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { getMonthSummary, getRecentTransactions } from "../core.js";
3
4export default {
5  name: "tree:finance-review",
6  emoji: "🔍",
7  label: "Finance Review",
8  bigMode: "tree",
9  hidden: true,
10
11  maxMessagesBeforeLoop: 6,
12  preserveContextOnLoop: true,
13
14  toolNames: [
15    "navigate-tree",
16    "get-tree-context",
17    "get-tree",
18    "get-node-notes",
19    "get-searched-notes-by-user",
20  ],
21
22  async buildSystemPrompt({ username, rootId, currentNodeId }) {
23    const finRoot = await findExtensionRoot(currentNodeId || rootId, "finance") || rootId;
24    const summary = finRoot ? await getMonthSummary(finRoot) : null;
25    const recent = finRoot ? await getRecentTransactions(finRoot, 20) : [];
26
27    const accountList = summary?.accounts?.length > 0
28      ? summary.accounts.map(a => `- ${a.name} (${a.accountType}): $${a.balance}`).join("\n")
29      : "No accounts.";
30
31    const categoryList = summary?.categories?.length > 0
32      ? summary.categories
33          .sort((a, b) => b.spentThisMonth - a.spentThisMonth)
34          .map(c => {
35            const pct = c.budget > 0 ? ` (${Math.round((c.spentThisMonth / c.budget) * 100)}%)` : "";
36            const over = c.budget > 0 && c.spentThisMonth > c.budget ? " OVER BUDGET" : "";
37            return `- ${c.name}: $${c.spentThisMonth}${c.budget > 0 ? `/$${c.budget}` : ""}${pct}${over}`;
38          }).join("\n")
39      : "";
40
41    const recentList = recent.length > 0
42      ? recent.slice(0, 15).map(n => `- ${n.content}`).join("\n")
43      : "No recent transactions.";
44
45    return `You are reviewing ${username}'s finances. Show the full picture. Be honest.
46
47ACCOUNTS:
48${accountList}
49
50${categoryList ? `SPENDING BY CATEGORY (this month):\n${categoryList}\n` : ""}
51TOTAL SPENT: $${summary?.totalSpent || 0}
52${summary?.totalBudget > 0 ? `TOTAL BUDGET: $${summary.totalBudget}\nREMAINING: $${summary.budgetRemaining}` : ""}
53
54RECENT TRANSACTIONS:
55${recentList}
56
57BEHAVIOR:
58- Answer their question directly with numbers.
59- "How much did I spend on food?" = look at Food category total.
60- "What's my balance?" = show all accounts.
61- "How am I doing this month?" = compare spending to budgets, highlight overages.
62- Flag concerning patterns: overspending in one category, declining balances, no savings.
63- Be factual. Don't moralize. Just show the numbers and let them decide.
64- Never expose node IDs or metadata to the user.`.trim();
65  },
66};
67
1/**
2 * Finance Dashboard
3 *
4 * Builds from getMonthSummary() data. Renders via the generic app dashboard.
5 * Accounts show balances. Categories show spending vs budget.
6 * Recent transactions from the log.
7 */
8
9import { renderAppDashboard } from "../../html-rendering/html/appDashboard.js";
10
11const CATEGORY_COLORS = {
12  Food: "#48bb78",
13  Housing: "#667eea",
14  Transport: "#f6ad55",
15  Health: "#fc8181",
16  Entertainment: "#a78bfa",
17  Shopping: "#ed64a6",
18  Bills: "#4fd1c5",
19  Other: "#718096",
20};
21
22export function renderFinanceDashboard({ rootId, rootName, summary, recentTransactions, token, userId, inApp }) {
23  const s = summary || {};
24  const accounts = s.accounts || [];
25  const categories = s.categories || [];
26  const totalBalance = s.totalBalance || 0;
27  const totalSpent = s.totalSpent || 0;
28  const totalBudget = s.totalBudget || 0;
29  const budgetRemaining = s.budgetRemaining;
30
31  // Hero: total balance
32  const hero = {
33    value: `$${totalBalance.toLocaleString()}`,
34    label: totalBudget > 0
35      ? `$${totalSpent.toLocaleString()} spent of $${totalBudget.toLocaleString()} budget`
36      : `$${totalSpent.toLocaleString()} spent this month`,
37    color: budgetRemaining != null && budgetRemaining < 0 ? "#fc8181" : "#48bb78",
38    sub: budgetRemaining != null && budgetRemaining >= 0
39      ? `$${budgetRemaining.toLocaleString()} remaining`
40      : budgetRemaining != null
41        ? `$${Math.abs(budgetRemaining).toLocaleString()} over budget`
42        : null,
43  };
44
45  // Stats: account balances
46  const stats = accounts.map(a => ({
47    value: `$${a.balance.toLocaleString()}`,
48    label: a.name,
49  }));
50
51  // Bars: category spending vs budget
52  const bars = categories
53    .filter(c => c.spentThisMonth > 0 || c.budget > 0)
54    .sort((a, b) => b.spentThisMonth - a.spentThisMonth)
55    .map(c => ({
56      label: c.name,
57      current: c.spentThisMonth,
58      goal: c.budget || 0,
59      color: CATEGORY_COLORS[c.name] || "#a78bfa",
60      unit: "$",
61    }));
62
63  // Cards: recent transactions
64  const txItems = (recentTransactions || []).slice(0, 20).map(n => ({
65    text: n.content || "Transaction",
66    sub: n.createdAt ? new Date(n.createdAt).toLocaleDateString() : null,
67  }));
68
69  const cards = [];
70  if (txItems.length > 0) {
71    cards.push({ title: "Recent Transactions", items: txItems });
72  }
73
74  // Account details card
75  if (accounts.length > 0) {
76    cards.push({
77      title: "Accounts",
78      items: accounts.map(a => ({
79        text: `${a.name} (${a.accountType})`,
80        detail: [`$${a.balance.toLocaleString()}`],
81      })),
82    });
83  }
84
85  return renderAppDashboard({
86    rootId,
87    rootName: rootName || "Finance",
88    token,
89    userId,
90    inApp: !!inApp,
91    subtitle: new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }),
92    hero,
93    stats,
94    bars,
95    cards,
96    chatBar: {
97      placeholder: "Log a transaction or ask about spending...",
98      endpoint: `/api/v1/root/${rootId}/chat`,
99    },
100    emptyState: totalSpent === 0 && accounts.every(a => a.balance === 0)
101      ? { title: "No transactions yet", message: "Tell me what you spent or earned. The tree tracks it." }
102      : null,
103  });
104}
105
0 stars
0 flags
React from the CLI: treeos ext star finance

Comments

Loading comments...

Post comments from the CLI: treeos ext comment finance "your comment"
Max 3 comments per extension. One star and one flag per user.