paisa-mcp 0.0.3 → 0.0.4

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,45 @@ function settlementEnrichedData(description, type, source) {
45080
45241
  return out;
45081
45242
  }
45082
45243
  // src/tools/transactions.ts
45244
+ async function runRescan(client) {
45245
+ const learningRes = await client.get("/api/household-learning");
45246
+ const bag = learningRes.data?.bag ?? {
45247
+ merchantAliases: [],
45248
+ personAliases: [],
45249
+ merchantRules: {},
45250
+ personRules: {}
45251
+ };
45252
+ const merchantsRes = await client.get("/api/merchants");
45253
+ const allMerchants = merchantsRes.data ?? [];
45254
+ const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45255
+ const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
45256
+ const personsRes = await client.get("/api/persons");
45257
+ const persons = (personsRes.data ?? []).filter((p2) => p2.name);
45258
+ const allRows = [];
45259
+ let offset = 0;
45260
+ const limit = 500;
45261
+ while (true) {
45262
+ const res = await client.get("/api/transactions", { limit, offset });
45263
+ const rows = res.data ?? [];
45264
+ allRows.push(...rows);
45265
+ if (!res.hasMore || rows.length === 0)
45266
+ break;
45267
+ offset += limit;
45268
+ }
45269
+ const plan = autoCategorizeFromDescriptions({ rows: allRows, bag, globalMerchants, localMerchants, persons });
45270
+ const links = plan.suggestions.filter((s2) => s2.categoryId).flatMap((s2) => s2.transactionIds.map((txId) => ({
45271
+ transactionId: txId,
45272
+ ...s2.entity.type === "merchant" ? { merchantId: s2.entity.merchantId } : { personId: s2.entity.personId },
45273
+ categoryId: s2.categoryId
45274
+ })));
45275
+ if (links.length > 0) {
45276
+ const CHUNK = 500;
45277
+ for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
45278
+ await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
45279
+ }
45280
+ }
45281
+ return { linked: links.length, unresolved: plan.unresolvedCount };
45282
+ }
45083
45283
  function registerTransactionTools(server, client, crypto3) {
45084
45284
  server.registerTool("get_transactions", {
45085
45285
  description: "Search and filter transactions. Returns paginated results with merchant and category details.",
@@ -45204,7 +45404,15 @@ function registerTransactionTools(server, client, crypto3) {
45204
45404
  granularity,
45205
45405
  transactions: rows
45206
45406
  });
45207
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
45407
+ const rescan = await runRescan(client);
45408
+ return {
45409
+ content: [
45410
+ {
45411
+ type: "text",
45412
+ text: JSON.stringify({ import: data, rescan })
45413
+ }
45414
+ ]
45415
+ };
45208
45416
  });
45209
45417
  server.registerTool("delete_transactions", {
45210
45418
  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 +45441,13 @@ function registerTransactionTools(server, client, crypto3) {
45233
45441
  const data = await client.delete("/api/transactions/bulk", body);
45234
45442
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
45235
45443
  });
45444
+ server.registerTool("rescan_transactions", {
45445
+ 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.",
45446
+ inputSchema: {}
45447
+ }, async () => {
45448
+ const result = await runRescan(client);
45449
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
45450
+ });
45236
45451
  server.registerTool("get_settlement_candidates", {
45237
45452
  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
45453
  inputSchema: {
@@ -45336,5 +45551,5 @@ main().catch((err) => {
45336
45551
  process.exit(1);
45337
45552
  });
45338
45553
 
45339
- //# debugId=8AA3117ABFC4C20064756E2164756E21
45554
+ //# debugId=E58A7271779D454264756E2164756E21
45340
45555
  //# sourceMappingURL=index.js.map