paisa-mcp 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -42597,8 +42597,7 @@ function registerGoalTools(server, client) {
42597
42597
  startDate: exports_external.string().optional().describe("[emi] YYYY-MM-DD"),
42598
42598
  endDate: exports_external.string().optional().describe("[emi] YYYY-MM-DD last payment date"),
42599
42599
  totalMonths: exports_external.number().optional().describe("[emi] Tenure in months"),
42600
- paidMonths: exports_external.number().optional().describe("[emi] Months already paid"),
42601
- monthlyContribution: exports_external.string().optional().describe("[sinking_fund] Monthly contribution (optional)")
42600
+ paidMonths: exports_external.number().optional().describe("[emi] Months already paid")
42602
42601
  }
42603
42602
  }, async ({ type, ...fields }) => {
42604
42603
  const require2 = (keys) => {
@@ -43279,6 +43278,7 @@ var MERCHANT_ALIASES = [
43279
43278
  ["LivPure", ["livpure smart homes", "livpure"]],
43280
43279
  ["HundredX", ["hundredx school", "hundredx"]]
43281
43280
  ];
43281
+ var GATEWAY_PREFIX = /^(paytm|payu|razorpay|cashfree|billdesk|easebuzz|rbl|pyu)\*/i;
43282
43282
  var aliasIndex = [];
43283
43283
  for (const [canonical, aliases] of MERCHANT_ALIASES) {
43284
43284
  for (const alias of aliases) {
@@ -43286,6 +43286,24 @@ for (const [canonical, aliases] of MERCHANT_ALIASES) {
43286
43286
  }
43287
43287
  }
43288
43288
  aliasIndex.sort((a, b) => b.alias.length - a.alias.length);
43289
+ function resolveAlias(name) {
43290
+ let lower = name.toLowerCase();
43291
+ const gwMatch = lower.match(GATEWAY_PREFIX);
43292
+ if (gwMatch) {
43293
+ lower = lower.slice(gwMatch[0].length).trim();
43294
+ }
43295
+ for (const { alias, canonical } of aliasIndex) {
43296
+ if (alias.length <= 4) {
43297
+ const re = new RegExp(`\\b${alias}\\b`);
43298
+ if (re.test(lower))
43299
+ return canonical;
43300
+ } else {
43301
+ if (lower.includes(alias))
43302
+ return canonical;
43303
+ }
43304
+ }
43305
+ return null;
43306
+ }
43289
43307
  // ../../node_modules/.bun/fuzzball@2.2.3/node_modules/fuzzball/dist/esm/fuzzball.esm.min.js
43290
43308
  var e;
43291
43309
  var t = typeof globalThis != "undefined" ? globalThis : typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : {};
@@ -45000,9 +45018,152 @@ var Yo = Ro.process_and_sort;
45000
45018
  var ea = Ro.unique_tokens;
45001
45019
  var ta = Ro.dedupe;
45002
45020
 
45021
+ // ../reconciliation/src/normalise.ts
45022
+ var CORPORATE_SUFFIXES = /\b(PRIVATE|PVT\.?|LIMITED|LTD\.?)\s*\w*/gi;
45023
+ function normaliseName(name) {
45024
+ return name.replace(CORPORATE_SUFFIXES, "").replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
45025
+ }
45026
+
45003
45027
  // ../reconciliation/src/entity.ts
45028
+ var FUZZBALL_THRESHOLD = 92;
45004
45029
  var _aliasReCache = new Map;
45030
+ function getAliasRegex(lower) {
45031
+ let re2 = _aliasReCache.get(lower);
45032
+ if (!re2) {
45033
+ const escaped = lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45034
+ re2 = new RegExp(`\\b${escaped}\\b`);
45035
+ _aliasReCache.set(lower, re2);
45036
+ }
45037
+ return re2;
45038
+ }
45005
45039
  var _normCache = new WeakMap;
45040
+ function cachedNormaliseName(opt) {
45041
+ let cached2 = _normCache.get(opt);
45042
+ if (cached2 === undefined) {
45043
+ cached2 = normaliseName(opt.name);
45044
+ _normCache.set(opt, cached2);
45045
+ }
45046
+ return cached2;
45047
+ }
45048
+ function matchUserAlias(normalisedName, aliases) {
45049
+ const sorted = [...aliases].sort((a2, b2) => b2.pattern.length - a2.pattern.length);
45050
+ for (const alias of sorted) {
45051
+ const lower = alias.pattern.toLowerCase();
45052
+ if (lower.length <= 4) {
45053
+ if (getAliasRegex(lower).test(normalisedName))
45054
+ return alias;
45055
+ } else if (normalisedName.includes(lower)) {
45056
+ return alias;
45057
+ }
45058
+ }
45059
+ return null;
45060
+ }
45061
+ function fuzzMatch(normalisedName, options) {
45062
+ let best = null;
45063
+ for (const opt of options) {
45064
+ const candidate = cachedNormaliseName(opt);
45065
+ if (!candidate)
45066
+ continue;
45067
+ const score = Math.max(Fo(normalisedName, candidate), Bo(normalisedName, candidate));
45068
+ if (score >= FUZZBALL_THRESHOLD && (!best || score > best.score)) {
45069
+ best = { row: opt, score };
45070
+ }
45071
+ }
45072
+ return best?.row ?? null;
45073
+ }
45074
+ function resolveMerchant(normalisedName, ctx) {
45075
+ const canonical = resolveAlias(normalisedName);
45076
+ if (canonical) {
45077
+ const normCanonical = normaliseName(canonical);
45078
+ const match = ctx.globalMerchants.find((m2) => cachedNormaliseName(m2) === normCanonical);
45079
+ if (match)
45080
+ return { type: "merchant", merchantId: match.id };
45081
+ }
45082
+ const userAlias = matchUserAlias(normalisedName, ctx.userMerchantAliases);
45083
+ if (userAlias)
45084
+ return { type: "merchant", merchantId: userAlias.merchantId };
45085
+ const fuzzMerchant = fuzzMatch(normalisedName, [...ctx.globalMerchants, ...ctx.localMerchants]);
45086
+ if (fuzzMerchant)
45087
+ return { type: "merchant", merchantId: fuzzMerchant.id };
45088
+ return null;
45089
+ }
45090
+ function resolvePerson(normalisedName, ctx) {
45091
+ const userAlias = matchUserAlias(normalisedName, ctx.userPersonAliases);
45092
+ if (userAlias)
45093
+ return { type: "person", personId: userAlias.personId };
45094
+ const fuzzPerson = fuzzMatch(normalisedName, ctx.persons);
45095
+ if (fuzzPerson)
45096
+ return { type: "person", personId: fuzzPerson.id };
45097
+ return null;
45098
+ }
45099
+ function resolveEntity(name, hint, ctx) {
45100
+ const normalised = normaliseName(name);
45101
+ if (!normalised)
45102
+ return null;
45103
+ if (hint === "person") {
45104
+ return resolvePerson(normalised, ctx) ?? resolveMerchant(normalised, ctx) ?? null;
45105
+ }
45106
+ if (hint === "merchant") {
45107
+ return resolveMerchant(normalised, ctx) ?? null;
45108
+ }
45109
+ return resolveMerchant(normalised, ctx) ?? resolvePerson(normalised, ctx) ?? null;
45110
+ }
45111
+
45112
+ // ../reconciliation/src/categorize.ts
45113
+ function autoCategorizeFromDescriptions(ctx) {
45114
+ const resolveCtx = {
45115
+ globalMerchants: ctx.globalMerchants,
45116
+ localMerchants: ctx.localMerchants,
45117
+ persons: ctx.persons,
45118
+ userMerchantAliases: ctx.bag.merchantAliases,
45119
+ userPersonAliases: ctx.bag.personAliases
45120
+ };
45121
+ const groups = new Map;
45122
+ let unresolvedCount = 0;
45123
+ let totalAffected = 0;
45124
+ for (const row of ctx.rows) {
45125
+ if (row.isTransfer || row.isIgnored)
45126
+ continue;
45127
+ let entity = null;
45128
+ if (row.merchantId)
45129
+ entity = { type: "merchant", merchantId: row.merchantId };
45130
+ else if (row.personId)
45131
+ entity = { type: "person", personId: row.personId };
45132
+ else
45133
+ entity = resolveEntity(row.description, "unknown", resolveCtx);
45134
+ if (!entity) {
45135
+ unresolvedCount += 1;
45136
+ continue;
45137
+ }
45138
+ const rule = entityRule(entity, ctx.bag);
45139
+ if (row.categoryId && rule && rule.categoryId === row.categoryId)
45140
+ continue;
45141
+ const needsLink = !row.merchantId && !row.personId;
45142
+ const needsCategory = !row.categoryId && rule != null;
45143
+ if (!needsLink && !needsCategory)
45144
+ continue;
45145
+ const key = entityKey(entity);
45146
+ const existing = groups.get(key);
45147
+ const suggestion = existing ?? {
45148
+ entity,
45149
+ source: rule ? "category-rule" : "alias-only",
45150
+ categoryId: rule?.categoryId ?? null,
45151
+ transactionIds: []
45152
+ };
45153
+ suggestion.transactionIds.push(row.id);
45154
+ groups.set(key, suggestion);
45155
+ totalAffected += 1;
45156
+ }
45157
+ return { suggestions: Array.from(groups.values()), totalAffected, unresolvedCount };
45158
+ }
45159
+ function entityKey(e2) {
45160
+ return e2.type === "merchant" ? `m:${e2.merchantId}` : `p:${e2.personId}`;
45161
+ }
45162
+ function entityRule(e2, bag) {
45163
+ if (e2.type === "merchant")
45164
+ return bag.merchantRules[e2.merchantId] ?? null;
45165
+ return bag.personRules[e2.personId] ?? null;
45166
+ }
45006
45167
  // ../reconciliation/src/settlement.ts
45007
45168
  var SETTLEMENT_PATTERNS = [
45008
45169
  /\bCRED\b.*\bPAYMENT\b/i,
@@ -45080,6 +45241,49 @@ function settlementEnrichedData(description, type, source) {
45080
45241
  return out;
45081
45242
  }
45082
45243
  // src/tools/transactions.ts
45244
+ async function runRescan(client) {
45245
+ const EMPTY_BAG2 = {
45246
+ merchantAliases: [],
45247
+ personAliases: [],
45248
+ merchantRules: {},
45249
+ personRules: {}
45250
+ };
45251
+ let bag = EMPTY_BAG2;
45252
+ try {
45253
+ const learningRes = await client.get("/api/household-learning");
45254
+ bag = learningRes.data?.bag ?? EMPTY_BAG2;
45255
+ } catch {}
45256
+ const merchantsRes = await client.get("/api/merchants");
45257
+ const allMerchants = merchantsRes.data ?? [];
45258
+ const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45259
+ const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45260
+ const personsRes = await client.get("/api/persons");
45261
+ const persons = (personsRes.data ?? []).filter((p2) => p2.name);
45262
+ const allRows = [];
45263
+ let offset = 0;
45264
+ const limit = 500;
45265
+ while (true) {
45266
+ const res = await client.get("/api/transactions", { limit, offset });
45267
+ const rows = res.data ?? [];
45268
+ allRows.push(...rows);
45269
+ if (!res.hasMore || rows.length === 0)
45270
+ break;
45271
+ offset += limit;
45272
+ }
45273
+ const plan = autoCategorizeFromDescriptions({ rows: allRows, bag, globalMerchants, localMerchants, persons });
45274
+ const links = plan.suggestions.filter((s2) => s2.categoryId).flatMap((s2) => s2.transactionIds.map((txId) => ({
45275
+ transactionId: txId,
45276
+ ...s2.entity.type === "merchant" ? { merchantId: s2.entity.merchantId } : { personId: s2.entity.personId },
45277
+ categoryId: s2.categoryId
45278
+ })));
45279
+ if (links.length > 0) {
45280
+ const CHUNK = 500;
45281
+ for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
45282
+ await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
45283
+ }
45284
+ }
45285
+ return { linked: links.length, unresolved: plan.unresolvedCount };
45286
+ }
45083
45287
  function registerTransactionTools(server, client, crypto3) {
45084
45288
  server.registerTool("get_transactions", {
45085
45289
  description: "Search and filter transactions. Returns paginated results with merchant and category details.",
@@ -45204,7 +45408,15 @@ function registerTransactionTools(server, client, crypto3) {
45204
45408
  granularity,
45205
45409
  transactions: rows
45206
45410
  });
45207
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
45411
+ const rescan = await runRescan(client);
45412
+ return {
45413
+ content: [
45414
+ {
45415
+ type: "text",
45416
+ text: JSON.stringify({ import: data, rescan })
45417
+ }
45418
+ ]
45419
+ };
45208
45420
  });
45209
45421
  server.registerTool("delete_transactions", {
45210
45422
  description: "Hard-delete transactions. This is PERMANENT and cannot be undone. " + "Provide exactly one of: `id` (single transaction), `ids` (a list of transactions), " + "or `filters` (deletes all matching transactions). Returns the number of deleted transactions " + "for the bulk paths.",
@@ -45233,6 +45445,13 @@ function registerTransactionTools(server, client, crypto3) {
45233
45445
  const data = await client.delete("/api/transactions/bulk", body);
45234
45446
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45235
45447
  });
45448
+ server.registerTool("rescan_transactions", {
45449
+ description: "Apply the household's saved learning rules (merchant aliases + category rules) to all uncategorized transactions. " + "Equivalent to the browser's auto-categorize flow. Run this after importing statements via MCP to populate merchants and categories.",
45450
+ inputSchema: {}
45451
+ }, async () => {
45452
+ const result = await runRescan(client);
45453
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
45454
+ });
45236
45455
  server.registerTool("get_settlement_candidates", {
45237
45456
  description: "Find bank debit transactions that look like bill payments for a specific credit card account. " + "Call this after importing a CC statement to identify which existing bank debits should be marked " + "as transfers (to avoid double-counting expenses). Returns transactions with their account name, " + "date, amount, and description.",
45238
45457
  inputSchema: {
@@ -45336,5 +45555,5 @@ main().catch((err) => {
45336
45555
  process.exit(1);
45337
45556
  });
45338
45557
 
45339
- //# debugId=8AA3117ABFC4C20064756E2164756E21
45558
+ //# debugId=66937CD8E27A6C8364756E2164756E21
45340
45559
  //# sourceMappingURL=index.js.map